// It seems best to make this an unowned window for two reasons:
// 1) It will get its own task bar icon then, which is usually desirable for cases where
// there are several progress/splash windows or the window is monitoring something.
// 2) The progress/splash window won't prevent the main window from being used (owned windows
// prevent their owners from ever becoming active).
// However, it seems likely that some users would want the above to be configurable,
// so now there is an option to change this behavior.
HWND dialog_owner = THREAD_DIALOG_OWNER; // Resolve macro only once to reduce code size.
if (!(splash.hwnd = CreateWindowEx(exstyle, WINDOW_CLASS_SPLASH, aTitle, style, xpos, ypos
, main_width, main_height, owned ? (dialog_owner ? dialog_owner : g_hWnd) : NULL // v1.0.35.01: For flexibility, allow these windows to be owned by GUIs via +OwnDialogs.
, NULL, g_hInstance, NULL)))
return FAIL; // No error msg since so rare.
if ((style & WS_SYSMENU) || !owned)
{
// Setting the small icon puts it in the upper left corner of the dialog window.
// Setting the big icon makes the dialog show up correctly in the Alt-Tab menu (but big seems to
// have no effect unless the window is unowned, i.e. it has a button on the task bar).
: LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED)); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
// Don't call GetCursorPos() unless absolutely needed because it seems to mess
// up double-click timing, at least on XP. UPDATE: Is isn't GetCursorPos() that's
// interfering with double clicks, so it seems it must be the displaying of the ToolTip
// window itself.
GetCursorPos(&pt_cursor);
pt.x = pt_cursor.x + 16; // Set default spot to be near the mouse cursor.
pt.y = pt_cursor.y + 16; // Use 16 to prevent the tooltip from overlapping large cursors.
// Update: Below is no longer needed due to a better fix further down that handles multi-line tooltips.
// 20 seems to be about the right amount to prevent it from "warping" to the top of the screen,
// at least on XP:
//if (pt.y > dtw.bottom - 20)
// pt.y = dtw.bottom - 20;
}
RECT rect = {0};
if ((*aX || *aY) && !(g.CoordMode & COORD_MODE_TOOLTIP)) // Need the rect.
{
if (!GetWindowRect(GetForegroundWindow(), &rect))
return OK; // Don't bother setting ErrorLevel with this command.
}
//else leave all of rect's members initialized to zero.
// This will also convert from relative to screen coordinates if rect contains non-zero values:
if (*aX)
pt.x = ATOI(aX) + rect.left;
if (*aY)
pt.y = ATOI(aY) + rect.top;
TOOLINFO ti = {0};
ti.cbSize = sizeof(ti) - sizeof(void *); // Fixed for v1.0.36.05: Tooltips fail to work on Win9x and probably NT4/2000 unless the size for the *lpReserved member in _WIN32_WINNT 0x0501 is omitted.
ti.uFlags = TTF_TRACK;
ti.lpszText = aText;
// Note that the ToolTip won't work if ti.hwnd is assigned the HWND from GetDesktopWindow().
// All of ti's other members are left at NULL/0, including the following:
g_clip.Close(); // Close the clipboard and free the memory.
output_var.Close(); // NOTE: Length() was already set properly by Assign() above. In case it's the clipboard, though currently it can't be since that would auto-detect as the reverse direction.
if (!char_count)
return output_var.Assign(); // Make non-clipboard output_var blank to indicate failure.
return OK;
case TRANS_CMD_HTML:
{
// These are the encoding-neutral translations for ASC 128 through 255 as shown by Dreamweaver.
// It's possible that using just the number convention (e.g. through ÿ) would be
// more appropriate for some users, but that mode can be added in the future if it is ever
// needed (by passing a mode setting for aValue2):
// Determine how long the result string will be so that the output variable can be expanded
// to handle it:
VarSizeType length;
UCHAR *ucp;
for (length = 0, ucp = (UCHAR *)aValue1; *ucp; ++ucp)
{
switch(*ucp)
{
case '"': // "
length += 6;
break;
case '&': // &
case '\n': // <br>\n
length += 5;
break; // v1.0.45: Added missing break. This had caused incorrect lengths inside some variables, which led to problems in places that relied on the accuracy of the internal lengths.
case '<': // <
case '>': // >
length += 4;
break; // v1.0.45: Added missing break.
default:
if (*ucp > 127)
length += (VarSizeType)strlen(sHtml[*ucp - 128]) + 2; // +2 for the leading '&' and the trailing ';'.
else
++length;
}
}
// Set up the var, enlarging it if necessary. If the output_var is of type VAR_CLIPBOARD,
// this call will set up the clipboard for writing:
if (output_var.Assign(NULL, length) != OK)
return FAIL; // It already displayed the error.
char *contents = output_var.Contents(); // For performance and tracking.
// Translate the text to HTML:
for (ucp = (UCHAR *)aValue1; *ucp; ++ucp)
{
switch(*ucp)
{
case '"': // "
strcpy(contents, """);
contents += 6;
break;
case '&': // &
strcpy(contents, "&");
contents += 5;
break;
case '\n': // <br>\n
strcpy(contents, "<br>\n");
contents += 5;
break;
case '<': // <
strcpy(contents, "<");
contents += 4;
break;
case '>': // >
strcpy(contents, ">");
contents += 4;
break;
default:
if (*ucp > 127)
{
*contents++ = '&'; // v1.0.40.02
strcpy(contents, sHtml[*ucp - 128]);
contents += strlen(contents); // Added as a fix in v1.0.41 (broken in v1.0.40.02).
*contents++ = ';'; // v1.0.40.02
}
else
*contents++ = *ucp;
}
}
*contents = '\0'; // Terminate the string.
return output_var.Close(); // In case it's the clipboard.
}
case TRANS_CMD_MOD:
if ( !(value_double2 = ATOF(aValue2)) ) // Divide by zero, set it to be blank to indicate the problem.
// v1.0.44.11: With Laszlo's help, negative bases are now supported as long as the exponent is not fractional.
// See SYM_POWER in script_expression.cpp for similar code and more comments.
value_double1 = ATOF(aValue1);
value_double2 = ATOF(aValue2);
bool value1_was_negative = (value_double1 < 0);
if (value_double1 == 0.0 && value_double2 < 0 // In essense, this is divide-by-zero.
|| value1_was_negative && qmathFmod(value_double2, 1.0) != 0.0) // Negative base but exponent isn't close enough to being an integer: unsupported (to simplify code).
return output_var.Assign(); // Return a consistent result (blank) rather than something that varies.
// Otherwise:
if (value1_was_negative)
value_double1 = -value_double1; // Force a positive due to the limitiations of qmathPow().
if (value1_was_negative && qmathFabs(qmathFmod(value_double2, 2.0)) == 1.0) // Negative base and exactly-odd exponent (otherwise, it can only be zero or even because if not it would have returned higher above).
return output_var.Assign((__int64)(result_double + (result_double > 0 ? 0.2 : -0.2))); // Fixed for v1.0.40.05: See comments in BIF_FloorCeil() for details.
case TRANS_CMD_ABS:
{
// Seems better to convert as string to avoid loss of 64-bit integer precision
// that would be caused by conversion to double. I think this will work even
// for negative hex numbers that are close to the 64-bit limit since they too have
// a minus sign when generated by the script (e.g. -0x1).
//result_double = qmathFabs(ATOF(aValue1));
//ASSIGN_BASED_ON_TYPE_SINGLE
char *cp = omit_leading_whitespace(aValue1); // i.e. caller doesn't have to have ltrimmed it.
if (*cp == '-')
return output_var.Assign(cp + 1); // Omit the first minus sign (simple conversion only).
// Otherwise, no minus sign, so just omit the leading whitespace for consistency:
return FAIL; // Never executed (increases maintainability and avoids compiler warning).
}
ResultType Line::Input()
// OVERVIEW:
// Although a script can have many concurrent quasi-threads, there can only be one input
// at a time. Thus, if an input is ongoing and a new thread starts, and it begins its
// own input, that input should terminate the prior input prior to beginning the new one.
// In a "worst case" scenario, each interrupted quasi-thread could have its own
// input, which is in turn terminated by the thread that interrupts it. Every time
// this function returns, it must be sure to set g_input.status to INPUT_OFF beforehand.
// This signals the quasi-threads beneath, when they finally return, that their input
// was terminated due to a new input that took precedence.
{
if (g_os.IsWin9x()) // v1.0.44.14: For simplicity, do nothing on Win9x rather than try to see if it actually supports the hook (such as if its some kind of emultated/hybrid OS).
return OK; // Could also set ErrorLevel to "Timeout" and output_var to be blank, but the benefits to backward compatibility seemed too dubious.
// Since other script threads can interrupt this command while it's running, it's important that
// this command not refer to sArgDeref[] and sArgVar[] anytime after an interruption becomes possible.
// This is because an interrupting thread usually changes the values to something inappropriate for this thread.
Var *output_var = OUTPUT_VAR; // See comment above.
if (!output_var)
{
// No output variable, which due to load-time validation means there are no other args either.
// This means that the user is specifically canceling the prior input (if any). Thus, our
// ErrorLevel here is set to 1 or 0, but the prior input's ErrorLevel will be set to "NewInput"
if (vk) // A valid virtual key code was discovered above.
{
end_vk[vk] |= END_KEY_ENABLED; // Use of |= is essential for cases such as ";:".
// Insist the shift key be down to form genuinely different symbols --
// namely punctuation marks -- but not for alphabetic chars. In the
// future, an option can be added to the Options param to treat
// end chars as case sensitive (if there is any demand for that):
if (*single_char_string && !IsCharAlpha(*single_char_string)) // v1.0.46.05: Added check for "*single_char_string" so that non-single-char strings like {F9} work as end keys even when the Shift key is being held down (this fixes the behavior to be like it was in pre-v1.0.45).
{
// Now we know it's not alphabetic, and it's not a key whose name
// is longer than one char such as a function key or numpad number.
// That leaves mostly just the number keys (top row) and all
// punctuation chars, which are the ones that we want to be
// distinguished between shifted and unshifted:
if (modifiersLR & (MOD_LSHIFT | MOD_RSHIFT))
end_vk[vk] |= END_KEY_WITH_SHIFT;
else
end_vk[vk] |= END_KEY_WITHOUT_SHIFT;
}
}
} // for()
/////////////////////////////////////////////////
// Parse aMatchList into an array of key phrases:
/////////////////////////////////////////////////
char **realloc_temp; // Needed since realloc returns NULL on failure but leaves original block allocated.
g_input.MatchCount = 0; // Set default.
if (*aMatchList)
{
// If needed, create the array of pointers that points into MatchBuf to each match phrase:
if (!g_input.match)
{
if ( !(g_input.match = (char **)malloc(INPUT_ARRAY_BLOCK_SIZE * sizeof(char *))) )
return LineError(ERR_OUTOFMEM); // Short msg. since so rare.
g_input.MatchCountMax = INPUT_ARRAY_BLOCK_SIZE;
}
// If needed, create or enlarge the buffer that contains all the match phrases:
size_t aMatchList_length = ArgLength(4); // Performs better than strlen(aMatchList);
size_t space_needed = aMatchList_length + 1; // +1 for the final zero terminator.
// Rather than monitoring the timeout here, just wait for the incoming WM_TIMER message
// to take effect as a TimerProc() call during the MsgSleep():
MsgSleep();
if (g_input.status != INPUT_IN_PROGRESS)
break;
}
switch(g_input.status)
{
case INPUT_TIMED_OUT:
g_ErrorLevel->Assign("Timeout");
break;
case INPUT_TERMINATED_BY_MATCH:
g_ErrorLevel->Assign("Match");
break;
case INPUT_TERMINATED_BY_ENDKEY:
{
char key_name[128] = "EndKey:";
if (g_input.EndingRequiredShift)
{
// Since the only way a shift key can be required in our case is if it's a key whose name
// is a single char (such as a shifted punctuation mark), use a diff. method to look up the
// key name based on fact that the shift key was down to terminate the input. We also know
// that the key is an EndingVK because there's no way for the shift key to have been
// required by a scan code based on the logic (above) that builds the end_key arrays.
// MSDN: "Typically, ToAscii performs the translation based on the virtual-key code.
// In some cases, however, bit 15 of the uScanCode parameter may be used to distinguish
// between a key press and a key release. The scan code is used for translating ALT+
// number key combinations.
BYTE state[256] = {0};
state[VK_SHIFT] |= 0x80; // Indicate that the neutral shift key is down for conversion purposes.
Get_active_window_keybd_layout // Defines the variable active_window_keybd_layout for use below.
int count = ToAsciiEx(g_input.EndingVK, vk_to_sc(g_input.EndingVK), (PBYTE)&state // Nothing is done about ToAsciiEx's dead key side-effects here because it seems to rare to be worth it (assuming its even a problem).
, (LPWORD)(key_name + 7), g_MenuIsVisible ? 1 : 0, active_window_keybd_layout); // v1.0.44.03: Changed to call ToAsciiEx() so that active window's layout can be specified (see hook.cpp for details).
// Set the defaults that will be in effect unless overridden by options:
KeyEventTypes event_type = KEYDOWNANDUP;
bool position_mode = false;
bool do_activate = true;
// These default coords can be overridden either by aOptions or aControl's X/Y mode:
POINT click = {COORD_UNSPECIFIED, COORD_UNSPECIFIED};
for (char *cp = aOptions; *cp; ++cp)
{
switch(toupper(*cp))
{
case 'D':
event_type = KEYDOWN;
break;
case 'U':
event_type = KEYUP;
break;
case 'N':
// v1.0.45:
// It was reported (and confirmed through testing) that this new NA mode (which avoids
// AttachThreadInput() and SetActiveWindow()) improves the reliability of ControlClick when
// the user is moving the mouse fairly quickly at the time the command tries to click a button.
// In addition, the new mode avoids activating the window, which tends to happen otherwise.
// HOWEVER, the new mode seems no more reliable than the old mode when the target window is
// the active window. In addition, there may be side-effects of the new mode (I caught it
// causing Notepad's Save-As dialog to hang once, during the display of its "Overwrite?" dialog).
// ALSO, SetControlDelay -1 seems to fix the unreliability issue as well (independently of NA),
// though it might not work with some types of windows/controls (thus, for backward
// compatibility, ControlClick still obeys SetControlDelay).
if (toupper(cp[1]) == 'A')
{
cp += 1; // Add 1 vs. 2 to skip over the rest of the letters in this option word.
do_activate = false;
}
break;
case 'P':
if (!strnicmp(cp, "Pos", 3))
{
cp += 2; // Add 2 vs. 3 to skip over the rest of the letters in this option word.
position_mode = true;
}
break;
// For the below:
// Use atoi() vs. ATOI() to avoid interpreting something like 0x01D as hex
// when in fact the D was meant to be an option letter:
case 'X':
click.x = atoi(cp + 1); // Will be overridden later below if it turns out that position_mode is in effect.
break;
case 'Y':
click.y = atoi(cp + 1); // Will be overridden later below if it turns out that position_mode is in effect.
break;
}
}
// It's debatable, but might be best for flexibility (and backward compatbility) to allow target_window to itself
// be a control (at least for the position_mode handler below). For example, the script may have called SetParent
// to make a top-level window the child of some other window, in which case this policy allows it to be seen like
// a non-child.
HWND control_window = position_mode ? NULL : ControlExist(target_window, aControl); // This can return target_window itself for cases such as ahk_id %ControlHWND%.
if (!control_window) // Even if position_mode is false, the below is still attempted, as documented.
{
// New section for v1.0.24. But only after the above fails to find a control do we consider
// whether aControl contains X and Y coordinates. That way, if a control class happens to be
// named something like "X1 Y1", it will still be found by giving precedence to class names.
point_and_hwnd_type pah = {0};
// Parse the X an Y coordinates in a strict way to reduce ambiguity with control names and also
// to keep the code simple.
char *cp = omit_leading_whitespace(aControl);
if (toupper(*cp) != 'X')
return OK; // Let ErrorLevel tell the story.
++cp;
if (!*cp)
return OK;
pah.pt.x = ATOI(cp);
if ( !(cp = StrChrAny(cp, " \t")) ) // Find next space or tab (there must be one for it to be considered valid).
return OK;
cp = omit_leading_whitespace(cp + 1);
if (!*cp || toupper(*cp) != 'Y')
return OK;
++cp;
if (!*cp)
return OK;
pah.pt.y = ATOI(cp);
// The passed-in coordinates are always relative to target_window's upper left corner because offering
// an option for absolute/screen coordinates doesn't seem useful.
RECT rect;
GetWindowRect(target_window, &rect);
pah.pt.x += rect.left; // Convert to screen coordinates.
pah.pt.y += rect.top;
EnumChildWindows(target_window, EnumChildFindPoint, (LPARAM)&pah); // Find topmost control containing point.
// If no control is at this point, try posting the mouse event message(s) directly to the
// parent window to increase the flexibility of this feature:
HWND control_window = target_window ? ControlExist(target_window, aControl) : NULL; // This can return target_window itself for cases such as ahk_id %ControlHWND%.
if (!control_window)
{
if (output_var_x)
output_var_x->Assign();
if (output_var_y)
output_var_y->Assign();
if (output_var_width)
output_var_width->Assign();
if (output_var_height)
output_var_height->Assign();
return OK;
}
RECT parent_rect, child_rect;
// Realistically never fails since DetermineTargetWindow() and ControlExist() should always yield
, &parent_rect); // v1.0.44.13: Above was fixed to allow for the fact that target_window might be the control itself (e.g. via ahk_id %ControlHWND%). See ControlMove for details.
GetWindowRect(control_window, &child_rect);
if (output_var_x && !output_var_x->Assign(child_rect.left - parent_rect.left))
return FAIL;
if (output_var_y && !output_var_y->Assign(child_rect.top - parent_rect.top))
return FAIL;
if (output_var_width && !output_var_width->Assign(child_rect.right - child_rect.left))
return FAIL;
if (output_var_height && !output_var_height->Assign(child_rect.bottom - child_rect.top))
HWND control_window = target_window ? ControlExist(target_window, aControl) : NULL; // This can return target_window itself for cases such as ahk_id %ControlHWND%.
// Even if control_window is NULL, we want to continue on so that the output
// param is set to be the empty string, which is the proper thing to do
// rather than leaving whatever was in there before.
// Handle the output parameter. This section is similar to that in
// PerformAssign(). Note: Using GetWindowTextTimeout() vs. GetWindowText()
// because it is able to get text from more types of controls (e.g. large edit controls):
if (row_count < 1 || !col_count) // But don't return when col_count == -1 (i.e. always make the attempt when col count is undetermined).
return g_ErrorLevel->Assign(ERRORLEVEL_NONE); // No text in the control, so indicate success.
// ALLOCATE INTERPROCESS MEMORY FOR TEXT RETRIEVAL
HANDLE handle;
LPVOID p_remote_lvi; // Not of type LPLVITEM to help catch bugs where p_remote_lvi->member is wrongly accessed here in our process.
if ( !(p_remote_lvi = AllocInterProcMem(handle, LV_REMOTE_BUF_SIZE + sizeof(LVITEM), aHwnd)) ) // Allocate the right type of memory (depending on OS type). Allocate both the LVITEM struct and its internal string buffer in one go because MyVirtualAllocEx() is probably a high overhead call.
return OK; // Let ErrorLevel tell the story.
bool is_win9x = g_os.IsWin9x(); // Resolve once for possible slight perf./code size benefit.
// PREPARE LVI STRUCT MEMBERS FOR TEXT RETRIEVAL
LVITEM lvi_for_nt; // Only used for NT/2k/XP method.
LVITEM &local_lvi = is_win9x ? *(LPLVITEM)p_remote_lvi : lvi_for_nt; // Local is the same as remote for Win9x.
// Subtract 1 because of that nagging doubt about size vs. length. Some MSDN examples subtract one,
// such as TabCtrl_GetItem()'s cchTextMax:
local_lvi.cchTextMax = LV_REMOTE_BUF_SIZE - 1; // Note that LVM_GETITEM doesn't update this member to reflect the new length.
local_lvi.pszText = (char *)p_remote_lvi + sizeof(LVITEM); // The next buffer is the memory area adjacent to, but after the struct.
bool single_col_mode = (requested_col > -1 || col_count == -1); // Get only one column in these cases.
// ESTIMATE THE AMOUNT OF MEMORY NEEDED TO STORE ALL THE TEXT
// It's important to note that a ListView might legitimately have a collection of rows whose
// fields are all empty. Since it is difficult to know whether the control is truly owner-drawn
// (checking its style might not be enough?), there is no way to distinguish this condition
// from one where the control's text can't be retrieved due to being owner-drawn. In any case,
// this all-empty-field behavior simplifies the code and will be documented in the help file.
for (i = 0, next = -1, total_length = 0; i < row_count; ++i) // For each row:
{
if (is_selective)
{
// Fix for v1.0.37.01: Prevent an infinite loop that might occur if the target control no longer
// exists (perhaps having been closed in the middle of the operation) or is permanently hung.
// If GetLastError() were to return zero after the below, it would mean the function timed out.
// However, rather than checking and retrying, it seems better to abort the operation because:
// 1) Timeout should be quite rare.
// 2) Reduces code size.
// 3) Having a retry really should be accompanied by SLEEP_WITHOUT_INTERRUPTION because all this
// time our thread would not pumping messages (and worse, if the keyboard/mouse hooks are installed,
// mouse/key lag would occur).
if (!SendMessageTimeout(aHwnd, LVM_GETNEXTITEM, next, include_focused_only ? LVNI_FOCUSED : LVNI_SELECTED
, SMTO_ABORTIFHUNG, 2000, (PDWORD_PTR)&next) // Timed out or failed.
|| next == -1) // No next item. Relies on short-circuit boolean order.
break; // End of estimation phase (if estimate is too small, the text retrieval below will truncate it).
}
else
next = i;
for (local_lvi.iSubItem = (requested_col > -1) ? requested_col : 0 // iSubItem is which field to fetch. If it's zero, the item vs. subitem will be fetched.
; col_count == -1 || local_lvi.iSubItem < col_count // If column count is undetermined (-1), always make the attempt.
; ++local_lvi.iSubItem) // For each column:
{
if ((is_win9x || WriteProcessMemory(handle, p_remote_lvi, &local_lvi, sizeof(LVITEM), NULL)) // Relies on short-circuit boolean order.
//else timed out or failed, don't include the length in the estimate. Instead, the
// text-fetching routine below will ensure the text doesn't overflow the var capacity.
if (single_col_mode)
break;
}
}
// Add to total_length enough room for one linefeed per row, and one tab after each column
// except the last (formula verified correct, though it's inflated by 1 for safety). "i" contains the
// actual number of rows that will be transcribed, which might be less than row_count if is_selective==true.
total_length += i * (single_col_mode ? 1 : col_count);
// SET UP THE OUTPUT VARIABLE, ENLARGING IT IF NECESSARY
// If the aOutputVar is of type VAR_CLIPBOARD, this call will set up the clipboard for writing:
aOutputVar.Assign(NULL, (VarSizeType)total_length, true, false); // Since failure is extremely rare, continue onward using the available capacity.
char *contents = aOutputVar.Contents();
LRESULT capacity = (int)aOutputVar.Capacity(); // LRESULT avoids signed vs. unsigned compiler warnings.
if (capacity > 0) // For maintainability, avoid going negative.
--capacity; // Adjust to exclude the zero terminator, which simplifies things below.
// RETRIEVE THE TEXT FROM THE REMOTE LISTVIEW
// Start total_length at zero in case actual size is greater than estimate, in which case only a partial set of text along with its '\t' and '\n' chars will be written.
for (i = 0, next = -1, total_length = 0; i < row_count; ++i) // For each row:
{
if (is_selective)
{
// Fix for v1.0.37.01: Prevent an infinite loop (for details, see comments in the estimation phase above).
if (!SendMessageTimeout(aHwnd, LVM_GETNEXTITEM, next, include_focused_only ? LVNI_FOCUSED : LVNI_SELECTED
, SMTO_ABORTIFHUNG, 2000, (PDWORD_PTR)&next) // Timed out or failed.
|| next == -1) // No next item.
break; // See comment above for why unconditional break vs. continue.
}
else // Retrieve every row, so the "next" row becomes the "i" index.
next = i;
// Insert a linefeed before each row except the first:
if (i && total_length < capacity) // If we're at capacity, it will exit the loops when the next field is read.
{
*contents++ = '\n';
++total_length;
}
// iSubItem is which field to fetch. If it's zero, the item vs. subitem will be fetched:
for (local_lvi.iSubItem = (requested_col > -1) ? requested_col : 0
; col_count == -1 || local_lvi.iSubItem < col_count // If column count is undetermined (-1), always make the attempt.
; ++local_lvi.iSubItem) // For each column:
{
// Insert a tab before each column except the first and except when in single-column mode:
if (!single_col_mode && local_lvi.iSubItem && total_length < capacity) // If we're at capacity, it will exit the loops when the next field is read.
{
*contents++ = '\t';
++total_length;
}
if (!(is_win9x || WriteProcessMemory(handle, p_remote_lvi, &local_lvi, sizeof(LVITEM), NULL)) // Relies on short-circuit boolean order.
return OK; // Since above didn't return, it's a failure. Let ErrorLevel tell the story.
}
case WINSET_ENABLE:
case WINSET_DISABLE: // These are separate sub-commands from WINSET_STYLE because merely changing the WS_DISABLED style is usually not as effective as calling EnableWindow().
else if (!(*aTitle || *aText || *aExcludeTitle || *aExcludeText)
&& !(cmd == WINGET_CMD_LIST || cmd == WINGET_CMD_COUNT)) // v1.0.30.02/v1.0.30.03: Have "list"/"count" get all windows on the system when there are no parameters.
target_window = GetValidLastUsedWindow(g);
else
target_window_determined = false; // A different method is required.
// Used with WINGET_CMD_LIST to create an array (if needed). Make it longer than Max var name
// so that FindOrAddVar() will be able to spot and report var names that are too long:
, output_var.IsLocal() ? ALWAYS_USE_LOCAL : ALWAYS_USE_GLOBAL)) ) // Find or create element #1.
return FAIL; // It will have already displayed the error.
if (!array_item->AssignHWND(target_window))
return FAIL;
}
return output_var.Assign("1"); // 1 window found
}
// Otherwise, the target window(s) have not yet been determined and a special method
// is required to gather them.
return WinGetList(output_var, cmd, aTitle, aText, aExcludeTitle, aExcludeText); // Outsourced to avoid having a WindowSearch object on this function's stack.
// Store in hex format to aid in debugging scripts. Also, the color is always
// stored in RGB format, since that's what WinSet uses:
sprintf(buf, "0x%06X", bgr_to_rgb(color));
return output_var.Assign(buf);
}
else // This window does not have a transparent color (or it's not accessible to us, perhaps for reasons described at MSDN GetLayeredWindowAttributes()).
return output_var.Assign();
}
}
return FAIL; // Never executed (increases maintainability and avoids compiler warning).
control_list_type &cl = *(control_list_type *)lParam; // For performance and convenience.
char line[WINDOW_CLASS_SIZE + 5]; // +5 to allow room for the sequence number to be appended later below.
int line_length;
// cl.fetch_hwnds==true is a new mode in v1.0.43.06+ to help performance of AHK Window Info and other
// scripts that want to operate directly on the HWNDs.
if (cl.fetch_hwnds)
{
line[0] = '0';
line[1] = 'x';
line_length = 2 + (int)strlen(_ui64toa((unsigned __int64)aWnd, line + 2, 16));
}
else // The mode that fetches ClassNN vs. HWND.
{
// Note: IsWindowVisible(aWnd) is not checked because although Window Spy does not reveal
// hidden controls if the mouse happens to be hovering over one, it does include them in its
// sequence numbering (which is a relieve, since results are probably much more consistent
// then, esp. for apps that hide and unhide controls in response to actions on other controls).
if ( !(line_length = GetClassName(aWnd, line, WINDOW_CLASS_SIZE)) ) // Don't include the +5 extra size since that is reserved for seq. number.
return TRUE; // Probably very rare. Continue enumeration since Window Spy doesn't even check for failure.
// It has been verified that GetClassName()'s returned length does not count the terminator.
// Check if this class already exists in the class array:
int class_index;
for (class_index = 0; class_index < cl.total_classes; ++class_index)
if (!stricmp(cl.class_name[class_index], line)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales.
break;
if (class_index < cl.total_classes) // Match found.
{
++cl.class_count[class_index]; // Increment the number of controls of this class that have been found so far.
if (cl.class_count[class_index] > 99999) // Sanity check; prevents buffer overflow or number truncation in "line".
return TRUE; // Continue the enumeration.
}
else // No match found, so create new entry if there's room.
{
if (cl.total_classes == CL_MAX_CLASSES // No pointers left.
GetSystemPaletteEntries(tdc, 0, 256, (LPPALETTEENTRY)palette); // Even if failure can realistically happen, consequences of using uninitialized palette seem acceptable.
// Above: GetSystemPaletteEntries() is the only approach that provided the correct palette.
// The following other approaches didn't give the right one:
// GetDIBits(): The palette it stores in bmi.bmiColors seems completely wrong.
// GetPaletteEntries()+GetCurrentObject(hdc, OBJ_PAL): Returned only 20 entries rather than the expected 256.
// GetDIBColorTable(): I think same as above or maybe it returns 0.
// The following section is necessary because apparently each new row in the region starts on
// a DWORD boundary. So if the number of pixels in each row isn't an exact multiple of 4, there
// are between 1 and 3 zero-bytes at the end of each row.
int remainder = aWidth % 4;
int empty_bytes_at_end_of_each_row = remainder ? (4 - remainder) : 0;
// Start at the last RGB slot and the last color index slot:
BYTE *byte = (BYTE *)image_pixel + image_pixel_count - 1 + (aHeight * empty_bytes_at_end_of_each_row); // Pointer to 8-bit color indices.
if (use_rgb) // aColorBGR currently contains an RGB value.
{
aColorRGB = aColorBGR;
aColorBGR = rgb_to_bgr(aColorBGR);
}
else
aColorRGB = rgb_to_bgr(aColorBGR); // rgb_to_bgr() also converts in the reverse direction, i.e. bgr_to_rgb().
// Many of the following sections are similar to those in ImageSearch(), so they should be
// maintained together.
Var *output_var_x = ARGVAR1; // Ok if NULL.
Var *output_var_y = aIsPixelGetColor ? NULL : ARGVAR2; // Ok if NULL. ARGVARRAW2 wouldn't be safe because load-time validation requires a min of only zero parameters to allow the output variables to be left blank.
g_ErrorLevel->Assign(aIsPixelGetColor ? ERRORLEVEL_ERROR : ERRORLEVEL_ERROR2); // Set default ErrorLevel. 2 means error other than "color not found".
if (output_var_x)
output_var_x->Assign(); // Init to empty string regardless of whether we succeed here.
if (output_var_y)
output_var_y->Assign(); // Same.
RECT rect = {0};
if (!(g.CoordMode & COORD_MODE_PIXEL)) // Using relative vs. screen coordinates.
{
if (!GetWindowRect(GetForegroundWindow(), &rect))
return OK; // Let ErrorLevel tell the story.
aLeft += rect.left;
aTop += rect.top;
aRight += rect.left; // Add left vs. right because we're adjusting based on the position of the window.
aBottom += rect.top; // Same.
}
if (aVariation < 0)
aVariation = 0;
if (aVariation > 255)
aVariation = 255;
// Allow colors to vary within the spectrum of intensity, rather than having them
// wrap around (which doesn't seem to make much sense). For example, if the user specified
// a variation of 5, but the red component of aColorBGR is only 0x01, we don't want red_low to go
// below zero, which would cause it to wrap around to a very intense red color:
if ( !(screen_pixel = getbits(hbitmap_screen, sdc, screen_width, screen_height, screen_is_16bit)) )
goto end;
LONG image_pixel_count = image_width * image_height;
LONG screen_pixel_count = screen_width * screen_height;
int i, j, k, x, y; // Declaring as "register" makes no performance difference with current compiler, so let the compiler choose which should be registers.
// If either is 16-bit, convert *both* to the 16-bit-compatible 32-bit format:
if (image_is_16bit || screen_is_16bit)
{
if (trans_color != CLR_NONE)
trans_color &= 0x00F8F8F8; // Convert indicated trans-color to be compatible with the conversion below.
for (i = 0; i < screen_pixel_count; ++i)
screen_pixel[i] &= 0x00F8F8F8; // Highest order byte must be masked to zero for consistency with use of 0x00FFFFFF below.
for (i = 0; i < image_pixel_count; ++i)
image_pixel[i] &= 0x00F8F8F8; // Same.
}
// v1.0.44.03: The below is now done even for variation>0 mode so its results are consistent with those of
// non-variation mode. This is relied upon by variation=0 mode but now also by the following line in the
// variation>0 section:
// || image_pixel[j] == trans_color
// Without this change, there are cases where variation=0 would find a match but a higher variation
// (for the same search) wouldn't.
for (i = 0; i < image_pixel_count; ++i)
image_pixel[i] &= 0x00FFFFFF;
// Search the specified region for the first occurrence of the image:
if (aVariation < 1) // Caller wants an exact match.
{
// Concerning the following use of 0x00FFFFFF, the use of 0x00F8F8F8 above is related (both have high order byte 00).
// The following needs to be done only when shades-of-variation mode isn't in effect because
// shades-of-variation mode ignores the high-order byte due to its use of macros such as GetRValue().
// This transformation incurs about a 15% performance decrease (percentage is fairly constant since
// it is proportional to the search-region size, which tends to be much larger than the search-image and
// is therefore the primary determination of how long the loops take). But it definitely helps find images
// more successfully in some cases. For example, if a PNG file is displayed in a GUI window, this
// transformation allows certain bitmap search-images to be found via variation==0 when they otherwise
// would require variation==1 (possibly the variation==1 success is just a side-effect of it
// ignoring the high-order byte -- maybe a much higher variation would be needed if the high
// order byte were also subject to the same shades-of-variation analysis as the other three bytes [RGB]).
for (i = 0; i < screen_pixel_count; ++i)
screen_pixel[i] &= 0x00FFFFFF;
for (i = 0; i < screen_pixel_count; ++i)
{
// Unlike the variation-loop, the following one uses a first-pixel optimization to boost performance
// by about 10% because it's only 3 extra comparisons and exact-match mode is probably used more often.
// Before even checking whether the other adjacent pixels in the region match the image, ensure
// the image does not extend past the right or bottom edges of the current part of the search region.
// This is done for performance but more importantly to prevent partial matches at the edges of the
// search region from being considered complete matches.
// The following check is ordered for short-circuit performance. In addition, image_mask, if
// non-NULL, is used to determine which pixels are transparent within the image and thus should
// match any color on the screen.
if ((screen_pixel[i] == image_pixel[0] // A screen pixel has been found that matches the image's first pixel.
|| image_mask && image_mask[0] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[0] == trans_color) // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
&& image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
&& image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
{
// Check if this candidate region -- which is a subset of the search region whose height and width
// matches that of the image -- is a pixel-for-pixel match of the image.
for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
{
if (!(found = (screen_pixel[k] == image_pixel[j] // At least one pixel doesn't match, so this candidate is discarded.
|| image_mask && image_mask[j] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[j] == trans_color))) // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
break;
if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
++k;
else // We're starting a new row of the image.
{
x = 0; // Return to the leftmost column of the image.
++y; // Move one row downward in the image.
// Move to the next row within the current-candiate region (not the entire search region).
// This is done by moving vertically downward from "i" (which is the upper-left pixel of the
// current-candidate region) by "y" rows.
k = i + y*screen_width; // Verified correct.
}
}
if (found) // Complete match found.
break;
}
}
}
else // Allow colors to vary by aVariation shades; i.e. approximate match is okay.
{
// The following section is part of the first-pixel-check optimization that improves performance by
// 15% or more depending on where and whether a match is found. This section and one the follows
// later is commented out to reduce code size.
// Set high/low range for the first pixel of the image since it is the pixel most often checked
// (i.e. for performance).
//BYTE search_red1 = GetBValue(image_pixel[0]); // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
//BYTE search_green1 = GetGValue(image_pixel[0]);
//BYTE search_blue1 = GetRValue(image_pixel[0]); // Same comment as above.
// The following loop is very similar to its counterpart above that finds an exact match, so maintain
// them together and see above for more detailed comments about it.
for (i = 0; i < screen_pixel_count; ++i)
{
// The following is commented out to trade code size reduction for performance (see comment above).
//red = GetBValue(screen_pixel[i]); // Because it's RGB vs. BGR, the B value is fetched, not R (though it doesn't matter as long as everything is internally consistent here).
//green = GetGValue(screen_pixel[i]);
//blue = GetRValue(screen_pixel[i]);
//if ((red >= red_low1 && red <= red_high1
// && green >= green_low1 && green <= green_high1
// && blue >= blue_low1 && blue <= blue_high1 // All three color components are a match, so this screen pixel matches the image's first pixel.
// || image_mask && image_mask[0] // Or: It's an icon's transparent pixel, which matches any color.
// || image_pixel[0] == trans_color) // This should be okay even if trans_color==CLR_NONE, since CLR none should never occur naturally in the image.
// && image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
// && image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
// Instead of the above, only this abbreviated check is done:
if (image_height <= screen_height - i/screen_width // Image is short enough to fit in the remaining rows of the search region.
&& image_width <= screen_width - i%screen_width) // Image is narrow enough not to exceed the right-side boundary of the search region.
{
// Since the first pixel is a match, check the other pixels.
for (found = true, x = 0, y = 0, j = 0, k = i; j < image_pixel_count; ++j)
{
search_red = GetBValue(image_pixel[j]);
search_green = GetGValue(image_pixel[j]);
search_blue = GetRValue(image_pixel[j]);
SET_COLOR_RANGE
red = GetBValue(screen_pixel[k]);
green = GetGValue(screen_pixel[k]);
blue = GetRValue(screen_pixel[k]);
if (!(found = red >= red_low && red <= red_high
&& green >= green_low && green <= green_high
&& blue >= blue_low && blue <= blue_high
|| image_mask && image_mask[j] // Or: It's an icon's transparent pixel, which matches any color.
|| image_pixel[j] == trans_color)) // This should be okay even if trans_color==CLR_NONE, since CLR_NONE should never occur naturally in the image.
break; // At least one pixel doesn't match, so this candidate is discarded.
if (++x < image_width) // We're still within the same row of the image, so just move on to the next screen pixel.
++k;
else // We're starting a new row of the image.
{
x = 0; // Return to the leftmost column of the image.
++y; // Move one row downward in the image.
k = i + y*screen_width; // Verified correct.
}
}
if (found) // Complete match found.
break;
}
}
}
if (!found) // Must override ErrorLevel to its new value prior to the label below.
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // "1" indicates search completed okay, but didn't find it.
end:
// If found==false when execution reaches here, ErrorLevel is already set to the right value, so just
// clean up then return.
ReleaseDC(NULL, hdc);
DeleteObject(hbitmap_image);
if (sdc)
{
if (sdc_orig_select) // i.e. the original call to SelectObject() didn't fail.
SelectObject(sdc, sdc_orig_select); // Probably necessary to prevent memory leak.
DeleteDC(sdc);
}
if (hbitmap_screen)
DeleteObject(hbitmap_screen);
if (image_pixel)
free(image_pixel);
if (image_mask)
free(image_mask);
if (screen_pixel)
free(screen_pixel);
if (!found) // Let ErrorLevel, which is either "1" or "2" as set earlier, tell the story.
return OK;
// Otherwise, success. Calculate xpos and ypos of where the match was found and adjust
// coords to make them relative to the position of the target window (rect will contain
// zeroes if this doesn't need to be done):
if (output_var_x && !output_var_x->Assign((aLeft + i%screen_width) - rect.left))
return FAIL;
if (output_var_y && !output_var_y->Assign((aTop + i/screen_width) - rect.top))
// See GuiWindowProc() for details about this first section:
LRESULT msg_reply;
if (g_MsgMonitorCount // Count is checked here to avoid function-call overhead.
&& (!g.CalledByIsDialogMessageOrDispatch || g.CalledByIsDialogMessageOrDispatchMsg != iMsg) // v1.0.44.11: If called by IsDialog or Dispatch but they changed the message number, check if the script is monitoring that new number.
break; // Let default proc handle it (since that's what used to happen, it seems safest).
//else fall through to the next case.
case WM_LBUTTONDBLCLK:
if (g_script.mTrayMenu->mDefault)
POST_AHK_USER_MENU(hWnd, g_script.mTrayMenu->mDefault->mMenuID, -1) // -1 flags it as a non-GUI menu item.
#ifdef AUTOHOTKEYSC
else if (g_script.mTrayMenu->mIncludeStandardItems && g_AllowMainWindow)
ShowMainWindow();
// else do nothing.
#else
else if (g_script.mTrayMenu->mIncludeStandardItems)
ShowMainWindow();
// else do nothing.
#endif
return 0;
case WM_RBUTTONUP:
// v1.0.30.03:
// Opening the menu upon UP vs. DOWN solves at least one set of problems: The fact that
// when the right mouse button is remapped as shown in the example below, it prevents
// the left button from being able to select a menu item from the tray menu. It might
// solve other problems also, and it seems fairly common for other apps to open the
// menu upon UP rather than down. Even Explorer's own context menus are like this.
// The following example is trivial and serves only to illustrate the problem caused
// by the old open-tray-on-mouse-down method:
//MButton::Send {RButton down}
//MButton up::Send {RButton up}
g_script.mTrayMenu->Display(false);
return 0;
} // Inner switch()
break;
} // case AHK_NOTIFYICON
case AHK_DIALOG: // User defined msg sent from our functions MsgBox() or FileSelectFile().
{
// Always call this to close the clipboard if it was open (e.g. due to a script
// line such as "MsgBox, %clipboard%" that got us here). Seems better just to
// do this rather than incurring the delay and overhead of a MsgSleep() call:
CLOSE_CLIPBOARD_IF_OPEN;
// Ensure that the app's top-most window (the modal dialog) is the system's
// foreground window. This doesn't use FindWindow() since it can hang in rare
// cases. And GetActiveWindow, GetTopWindow, GetWindow, etc. don't seem appropriate.
// So EnumWindows is probably the way to do it:
HWND top_box = FindOurTopDialog();
if (top_box)
{
// v1.0.33: The following is probably reliable since the AHK_DIALOG should
// be in front of any messages that would launch an interrupting thread. In other
// words, the "g" struct should still be the one that owns this MsgBox/dialog window.
g.DialogHWND = top_box; // This is used to work around an AHK_TIMEOUT issue in which a MsgBox that has only an OK button fails to deliver the Timeout indicator to the script.
SetForegroundWindowEx(top_box);
// Setting the big icon makes AutoHotkey dialogs more distinct in the Alt-tab menu.
// Unfortunately, it seems that setting the big icon also indirectly sets the small
// icon, or more precisely, that the dialog simply scales the large icon whenever
// a small one isn't available. This results in the FileSelectFile dialog's title
// being initially messed up (at least on WinXP) and also puts an unwanted icon in
// the title bar of each MsgBox. So for now it's disabled:
return 1; // "An application should return nonzero if it erases the background."
}
// Otherwise, it's a Progress window (or a SplashImage window with no picture):
if (!splash.hbrush) // Let DWP handle it.
break;
RECT clipbox;
GetClipBox((HDC)wParam, &clipbox);
FillRect((HDC)wParam, &clipbox, splash.hbrush);
return 1; // "An application should return nonzero if it erases the background."
}
} // switch()
break; // Let DWP handle it.
}
case WM_SETFOCUS:
if (hWnd == g_hWnd)
{
SetFocus(g_hWndEdit); // Always focus the edit window, since it's the only navigable control.
return 0;
}
break;
case WM_DRAWCLIPBOARD:
if (g_script.mOnClipboardChangeLabel) // In case it's a bogus msg, it's our responsibility to avoid posting the msg if there's no label to launch.
PostMessage(g_hWnd, AHK_CLIPBOARD_CHANGE, 0, 0); // It's done this way to buffer it when the script is uninterruptible, etc. v1.0.44: Post to g_hWnd vs. NULL so that notifications aren't lost when script is displaying a MsgBox or other dialog.
if (g_script.mNextClipboardViewer) // Will be NULL if there are no other windows in the chain.
if (!g_script.ActionExec("hh.exe", buf_temp, NULL, false))
{
// Try it without the hh.exe in case .CHM is associate with some other application
// in some OSes:
if (!g_script.ActionExec(buf_temp, "", NULL, false)) // Use "" vs. NULL to specify that there are no params at all.
MsgBox(buf_temp, 0, "Could not launch help file:");
}
return true;
case ID_TRAY_SUSPEND:
case ID_FILE_SUSPEND:
Line::ToggleSuspendState();
return true;
case ID_TRAY_PAUSE:
case ID_FILE_PAUSE:
if (g_nThreads > 0) // v1.0.37.06: Pausing the "idle thread" (which is not included in the thread count) is an easy means by which to disable all timers.
{
if (g.IsPaused)
--g_nPausedThreads;
else
++g_nPausedThreads;
}
else // Toggle the pause state of the idle thread.
g_script.ExitApp(EXIT_MENU); // More reliable than PostQuitMessage(), which has been known to fail in rare cases.
return true; // If there is an OnExit subroutine, the above might not actually exit.
case ID_VIEW_LINES:
ShowMainWindow(MAIN_MODE_LINES);
return true;
case ID_VIEW_VARIABLES:
ShowMainWindow(MAIN_MODE_VARS);
return true;
case ID_VIEW_HOTKEYS:
ShowMainWindow(MAIN_MODE_HOTKEYS);
return true;
case ID_VIEW_KEYHISTORY:
ShowMainWindow(MAIN_MODE_KEYHISTORY);
return true;
case ID_VIEW_REFRESH:
ShowMainWindow(MAIN_MODE_REFRESH);
return true;
case ID_HELP_WEBSITE:
if (!g_script.ActionExec("http://www.autohotkey.com", "", NULL, false))
MsgBox("Could not open URL http://www.autohotkey.com in default browser.");
return true;
default:
// See if this command ID is one of the user's custom menu items. Due to the possibility
// that some items have been deleted from the menu, can't rely on comparing
// aMenuItemID to g_script.mMenuItemCount in any way. Just look up the ID to make sure
// there really is a menu item for it:
if (!g_script.FindMenuItemByID(aMenuItemID)) // Do nothing, let caller try to handle it some other way.
return false;
// It seems best to treat the selection of a custom menu item in a way similar
// to how hotkeys are handled by the hook. See comments near the definition of
// POST_AHK_USER_MENU for more details.
POST_AHK_USER_MENU(aHwnd, aMenuItemID, aGuiIndex) // Send the menu's cmd ID and the window index (index is safer than pointer, since pointer might get deleted).
// Try to maintain a list here of all the ways the script can be uninterruptible
// at this moment in time, and whether that uninterruptibility should be overridden here:
// 1) YES: g_MenuIsVisible is true (which in turn means that the script is marked
// uninterruptible to prevent timed subroutines from running and possibly
// interfering with menu navigation): Seems impossible because apparently
// the WM_RBUTTONDOWN must first be returned from before we're called directly
// with the WM_COMMAND message corresponding to the menu item chosen by the user.
// In other words, g_MenuIsVisible will be false and the script thus will
// not be uninterruptible, at least not solely for that reason.
// 2) YES: A new hotkey or timed subroutine was just launched and it's still in its
// grace period. In this case, ExecUntil()'s call of PeekMessage() every 10ms
// or so will catch the item we just posted. But it seems okay to interrupt
// here directly in most such cases. InitNewThread(): Newly launched
// timed subroutine or hotkey subroutine.
// 3) YES: Script is engaged in an uninterruptible activity such as SendKeys(). In this
// case, since the user has managed to get the tray menu open, it's probably
// best to process the menu item with the same priority as if any other menu
// item had been selected, interrupting even a critical operation since that's
// probably what the user would want. SLEEP_WITHOUT_INTERRUPTION: SendKeys,
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
return length;
}
//////////////
// InputBox //
//////////////
ResultType InputBox(Var *aOutputVar, char *aTitle, char *aText, bool aHideInput, int aWidth, int aHeight
, int aX, int aY, double aTimeout, char *aDefault)
{
// Note: for maximum compatibility with existing AutoIt2 scripts, do not
// set ErrorLevel to ERRORLEVEL_ERROR when the user presses cancel. Instead,
// just set the output var to be blank.
if (g_nInputBoxes >= MAX_INPUTBOXES)
{
// Have a maximum to help prevent runaway hotkeys due to key-repeat feature, etc.
MsgBox("The maximum number of InputBoxes has been reached." ERR_ABORT);
return FAIL;
}
if (!aOutputVar) return FAIL;
if (!*aTitle)
// If available, the script's filename seems a much better title in case the user has
// Typically, the dialog box procedure should return TRUE if it processed the message,
// and FALSE if it did not. If the dialog box procedure returns FALSE, the dialog
// manager performs the default dialog operation in response to the message.
{
// See GuiWindowProc() for details about this first part:
LRESULT msg_reply;
if (g_MsgMonitorCount // Count is checked here to avoid function-call overhead.
&& (!g.CalledByIsDialogMessageOrDispatch || g.CalledByIsDialogMessageOrDispatchMsg != uMsg) // v1.0.44.11: If called by IsDialog or Dispatch but they changed the message number, check if the script is monitoring that new number.
: LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED)); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
*output_buf_marker++ = ' '; // Provide a space between time and date.
++output_buf_length;
}
else
format2 = format2_marker;
int buf_remaining_size = (int)(FT_MAX_OUTPUT_CHARS - output_buf_length);
int result;
if (format_type1 == FT_FORMAT_DATE) // DATE came first, so this one is TIME.
result = GetTimeFormat(lcid, time_flags, &st, format2, output_buf_marker, buf_remaining_size);
else
result = GetDateFormat(lcid, date_flags, &st, format2, output_buf_marker, buf_remaining_size);
if (!result)
output_buf[output_buf_length] = '\0'; // Ensure the first part is still terminated and just return that rather than nothing.
}
return output_var.Assign(output_buf);
}
ResultType Line::PerformAssign()
// Returns OK or FAIL. Caller has ensured that none of this line's derefs is a function-call.
{
Var *p_output_var; // Can't use OUTPUT_VAR or sArgVar here because ExpandArgs() isn't called prior to PerformAssign().
if ( !(p_output_var = ResolveVarOfArg(0)) ) // Fix for v1.0.46.07: Must do this check in case of illegal dynamically-build variable name.
return FAIL;
p_output_var = p_output_var->ResolveAlias(); // Resolve alias now to detect "source_is_being_appended_to_target" and perhaps other things.
Var &output_var = *p_output_var; // For performance.
// Now output_var.Type() must be clipboard or normal because otherwise load-time validation (or
// ResolveVarOfArg() in GetExpandedArgSize, if it's dynamic) would have prevented us from getting this far.
if (mArgc > 1 && ArgHasDeref(2)) // There is at least one deref in Arg #2.
{
// Can't use sArgVar here because ExecUntil() never calls ExpandArgs() for ACT_ASSIGN.
// For simplicity, we don't check that it's the only deref, nor whether it has any literal text
// around it, since those things aren't supported anyway.
Var *source_var;
if (source_var = mArg[1].deref[0].var) // Caller has ensured none of this line's derefs is a function-call, so var should always be the proper member of the union to check.
{
if (source_var->Type() == VAR_CLIPBOARDALL) // The caller is performing the special mode "Var = %ClipboardAll%".
return output_var.AssignClipboardAll(); // Outsourced to another function to help CPU cache hits/misses in this frequently-called function.
if (source_var->IsBinaryClip()) // Caller wants a variable with binary contents assigned (copied) to another variable (usually VAR_CLIPBOARD).
return output_var.AssignBinaryClip(*source_var); // Outsourced to another function to help CPU cache hits/misses in this frequently-called function.
}
}
// Otherwise (since above didn't return):
// Find out if output_var (the var being assigned to) is dereferenced (mentioned) in this line's
// second arg, which is the value to be assigned. If it isn't, things are much simpler.
// Note: Since Arg#2 for this function is never an output or an input variable, it is not
// necessary to check whether its the same variable as Arg#1 for this determination.
if (output_var.Type() != VAR_CLIPBOARD && mArgc > 1 // If types is VAR_CLIPBOARD, it can be used in the source deref(s) while also being the target -- without having to use the deref buffer -- because the clipboard has it's own temp buffer: the memory area to which the result is written. The prior content of the clipboard remains available in its other memory area until Commit() is called (i.e. long enough for our purposes).
&& mArg[1].deref) // ...and there's at least one deref in the arg.
{
// It has a second arg, which in this case is the value to be assigned to the var.
// Examine any derefs that the second arg has to see if output_var is mentioned.
// Also, calls to script functions aren't possible within these derefs because
// our caller has ensured there are no expressions, and thus no function calls,
// inside this line.
for (DerefType *deref = mArg[1].deref; deref->marker; ++deref)
{
if (deref->is_function) // Silent failure, for rare cases such ACT_ASSIGNEXPR calling us due to something like Clipboard:=SavedBinaryClipboard + fn(x) [which isn't valid for binary clipboard]
return FAIL;
if (source_is_being_appended_to_target)
{
// Check if target is mentioned more than once in source, e.g. Var = %Var%Some Text%Var%
// would be disqualified for the "fast append" method because %Var% occurs more than once.
if (deref->var->ResolveAlias() == p_output_var) // deref->is_function was checked above just in case.
{
source_is_being_appended_to_target = false;
break;
}
}
else
{
if (deref->var->ResolveAlias() == p_output_var) // deref->is_function was checked above just in case.
{
target_is_involved_in_source = true;
// The below disqualifies both of the following cases from the simple-append mode:
// Var = %OtherVar%%Var% ; Var is not the first item as required.
// Var = LiteralText%Var% ; Same.
if (deref->marker == mArg[1].text)
source_is_being_appended_to_target = true;
// And continue the loop to ensure that Var is not referenced more than once,
// e.g. Var = %Var%%Var% would be disqualified.
else
break;
}
}
}
}
// Note: It might be possible to improve performance in the case where
// the target variable is large enough to accommodate the new source data
// by moving memory around inside it. For example, Var1 = xxxxxVar1
// could be handled by moving the memory in Var1 to make room to insert
// the literal string. In addition to being quicker than the ExpandArgs()
// method, this approach would avoid the possibility of needing to expand the
// deref buffer just to handle the operation. However, if that is ever done,
// be sure to check that output_var is mentioned only once in the list of derefs.
// For example, something like this would probably be much easier to
// implement by using ExpandArgs(): Var1 = xxxx %Var1% %Var2% %Var1% xxxx.
// So the main thing to be possibly later improved here is the case where
// output_var is mentioned only once in the deref list (which as of v1.0.25,
// has been partially done via the concatenation improvement, e.g. Var = %Var%Text).
Var *arg_var[MAX_ARGS];
VarSizeType space_needed;
if (target_is_involved_in_source && !source_is_being_appended_to_target) // If true, output_var isn't the clipboard due to invariant: target_is_involved_in_source==false whenever output_var.Type()==VAR_CLIPBOARD.
{
if (ExpandArgs() != OK)
return FAIL;
// ARG2 now contains the dereferenced (literal) contents of the text we want to assign.
// Therefore, calling ArgLength() is safe now too (i.e. ExpandArgs set things up for it).
space_needed = (VarSizeType)ArgLength(2) + 1; // +1 for the zero terminator.
}
else
{
space_needed = GetExpandedArgSize(false, arg_var); // There's at most one arg to expand in this case.
if (space_needed == VARSIZE_ERROR)
return FAIL; // It will have already displayed the error.
}
// Now above has ensured that space_needed is at least 1 (it should not be zero because even
// the empty string uses up 1 char for its zero terminator). The below relies upon this fact.
if (space_needed < 2) // Variable is being assigned the empty string (or a deref that resolves to it).
return output_var.Assign(""); // If the var is of large capacity, this will also free its memory.
if (source_is_being_appended_to_target)
{
if (space_needed > output_var.Capacity())
{
// Since expanding the size of output_var while preserving its existing contents would
// likely be a slow operation, revert to the normal method rather than the fast-append
// mode. Expand the args then continue on normally to the below.
if (ExpandArgs(space_needed, arg_var) != OK) // In this case, both params were previously calculated by GetExpandedArgSize().
return FAIL;
}
else // there's enough capacity in output_var to accept the text to be appended.
target_is_involved_in_source = false; // Tell the below not to consider expanding the args.
}
char *contents;
if (target_is_involved_in_source) // output_var can't be clipboard due to invariant: target_is_involved_in_source==false whenever output_var.Type()==VAR_CLIPBOARD.
{
// It was already dereferenced above, so use ARG2, which points to the deref'ed contents of ARG2
// (i.e. the data to be assigned). I don't think there's any point to checking ARGVAR2!=NULL
// and if so passing ARGVAR2->LengthIgnoreBinaryClip, because when we're here, ExpandArgs() will
// have seen that it can't optimize it that way and thus it has fully expanded the variable into the buffer.
if (!output_var.Assign(ARG2)) // Don't pass it space_needed-1 as the length because space_needed might be a conservative estimate larger than the actual length+terminator.
return FAIL;
if (g.AutoTrim)
{
contents = output_var.Contents();
if (*contents)
{
VarSizeType &var_len = output_var.Length(); // Might help perf. slightly.
var_len = (VarSizeType)trim(contents, var_len); // Passing length to trim() is known to greatly improve performance for long strings.
}
}
return OK;
}
// Otherwise: target isn't involved in source (output_var isn't itself involved in the source data/parameter).
// First set everything up for the operation. If output_var is the clipboard, this
// will prepare the clipboard for writing. Update: If source is being appended
// to target using the simple method, we know output_var isn't the clipboard because the
// logic at the top of this function ensures that.
if (!source_is_being_appended_to_target)
if (output_var.Assign(NULL, space_needed - 1) != OK)
return FAIL;
// Expand Arg2 directly into the var. Note: If output_var is the clipboard,
// it's probably okay if the below actually writes less than the size of
// the mem that has already been allocated for the new clipboard contents
// That might happen due to a failure or size discrepancy between the
// deref size-estimate and the actual deref itself:
contents = output_var.Contents();
// This knows not to copy the first var-ref onto itself (for when source_is_being_appended_to_target is true).
// In addition, to reach this point, arg_var[0]'s value will already have been determined (possibly NULL)
// by GetExpandedArgSize():
char *one_beyond_contents_end = ExpandArg(contents, 1, arg_var[1]); // v1.0.45: Fixed arg_var[0] to be arg_var[1] (but this was only a performance issue).
if (!one_beyond_contents_end)
return FAIL; // ExpandArg() will have already displayed the error.
// Set the length explicitly rather than using space_needed because GetExpandedArgSize()
// sometimes returns a larger size than is actually needed (e.g. for ScriptGetCursor()):
return output_var.Close(); // i.e. Consider this function to be always successful unless this fails.
}
ResultType Line::StringReplace()
// v1.0.45: Revised to improve average-case performance and reduce memory utilization.
{
Var &output_var = *OUTPUT_VAR;
char *source = ARG2;
size_t length = ArgLength(2); // Going in, it's the haystack length. Later (coming out), it's the result length.
bool alternate_errorlevel = strcasestr(ARG5, "UseErrorLevel"); // This also implies replace-all.
UINT replacement_limit = (alternate_errorlevel || StrChrAny(ARG5, "1aA")) // This must be done in a way that recognizes "AllSlow" as meaning replace-all (even though the slow method itself is obsolete).
? UINT_MAX : 1;
// In case the strings involved are massive, free the output_var in advance of the operation to
// reduce memory load and avoid swapping (but only if output_var isn't the same address as the input_var).
if (output_var.Type() == VAR_NORMAL && source != output_var.Contents()) // It's compared this way in case ByRef/aliases are involved. This will detect even them.
output_var.Free();
//else source and dest are the same, so can't free the dest until after the operation.
// Note: The current implementation of StrReplace() should be able to handle any conceivable inputs
// without an empty string causing an infinite loop and without going infinite due to finding the
// search string inside of newly-inserted replace strings (e.g. replacing all occurrences
// of b with bcb would not keep finding b in the newly inserted bcd, infinitely).
, replacement_limit, -1, &dest, &length); // Length of haystack is passed to improve performance because ArgLength() can often discover it instantaneously.
if (!dest) // Failure due to out of memory.
return LineError(ERR_OUTOFMEM ERR_ABORT);
if (dest != source) // StrReplace() allocated new memory rather than returning "source" to us unaltered.
{
// v1.0.45: Take a shortcut for performance: Hang the newly allocated memory (populated by the callee)
// directly onto the variable, which saves a memcpy() over the old method (and possible other savings).
// AcceptNewMem() will shrink the memory for us, via _expand(), if there's a lot of extra/unused space in it.
output_var.AcceptNewMem(dest, (VarSizeType)length); // Tells the variable to adopt this memory as its new memory. Callee has set "length" for us.
// Above also handles the case where output_var is VAR_CLIPBOARD.
}
else // StrReplace gave us back "source" unaltered because no replacements were needed.
{
if (output_var.Type() == VAR_NORMAL)
{
// Technically the following check isn't necessary because Assign() also checks for it.
// But since StringReplace is a frequently-used command, checking it here seems worthwhile
// to avoid calling Assign().
if (source != output_var.Contents()) // It's compared this way in case ByRef/aliases are involved. This will detect even them.
output_var.Assign(source, (VarSizeType)length); // Callee has set "length" for us.
//else the unaltered result and output_var same the same address. Nothing needs to be done (for
// simplicity, not even the binary-clipboard attribute is removed if it happnes to be present).
}
else // output_var is of type VAR_CLIPBOARD.
if (ARGVARRAW2->Type() != VAR_CLIPBOARD) // Arg index #1 (the second arg) is a normal var or some read-only var.
output_var.Assign(source, (VarSizeType)length); // Callee has set "length" for us.
//else the unaltered result and output_var are both the clipboard. Nothing needs to be done.
}
if (alternate_errorlevel)
g_ErrorLevel->Assign((DWORD)found_count);
else // Use old ErrorLevel method for backward compatibility.
if (mAttribute != ATTR_NONE) // 1.0.46.10: Fixed to rely on loadtime's determination of whether ArrayName0 is truly local or global (only loadtime currently has any awareness of declarations, so the determination must be made there unless "ArrayName" itself is a dynamic variable, which seems too rare to worry about).
array0 = (Var *)mAttribute;
else
{
var_name_suffix[0] = '0';
var_name_suffix[1] = '\0';
// ALWAYS_PREFER_LOCAL below allows any existing local variable that matches array0's name
// (e.g. Array0) to be given preference over creating a new global variable if the function's
// mode is to assume globals:
if ( !(array0 = g_script.FindOrAddVar(var_name, 0, ALWAYS_PREFER_LOCAL)) )
return FAIL; // It will have already displayed the error.
}
int always_use = array0->IsLocal() ? ALWAYS_USE_LOCAL : ALWAYS_USE_GLOBAL;
if (!*aInputString) // The input variable is blank, thus there will be zero elements.
return array0->Assign("0"); // Store the count in the 0th element.
DWORD next_element_number;
Var *next_element;
if (*aDelimiterList) // The user provided a list of delimiters, so process the input variable normally.
// If there are no chars to the left of the delim, or if they were all in the list of omitted
// chars, the variable will be assigned the empty string:
if (!next_element->Assign(contents_of_next_element, (VarSizeType)element_length))
return FAIL;
// This is the only way out of the loop other than critical errors:
return array0->Assign(next_element_number); // Store the count of how many items were stored in the array.
}
}
}
// Otherwise aDelimiterList is empty, so store each char of aInputString in its own array element.
char *cp, *dp;
for (cp = aInputString, next_element_number = 1; *cp; ++cp)
{
for (dp = aOmitList; *dp; ++dp)
if (*cp == *dp) // This char is a member of the omitted list, thus it is not included in the output array.
break;
if (*dp) // Omitted.
continue;
_ultoa(next_element_number, var_name_suffix, 10);
if ( !(next_element = g_script.FindOrAddVar(var_name, 0, always_use)) )
return FAIL; // It will have already displayed the error.
if (!next_element->Assign(cp, 1))
return FAIL;
++next_element_number; // Only increment this if above didn't "continue".
}
return array0->Assign(next_element_number - 1); // Store the count of how many items were stored in the array.
}
ResultType Line::SplitPath(char *aFileSpec)
{
Var *output_var_name = ARGVAR2; // i.e. Param #2. Ok if NULL.
Var *output_var_dir = ARGVAR3; // Ok if NULL.
Var *output_var_ext = ARGVAR4; // Ok if NULL.
Var *output_var_name_no_ext = ARGVAR5; // Ok if NULL.
Var *output_var_drive = ARGVAR6; // Ok if NULL.
// For URLs, "drive" is defined as the server name, e.g. http://somedomain.com
char *name = "", *name_delimiter = NULL, *drive_end = NULL; // Set defaults to improve maintainability.
char *drive = omit_leading_whitespace(aFileSpec); // i.e. whitespace is considered for everything except the drive letter or server name, so that a pathless filename can have leading whitespace.
// Don't use _splitpath() since it supposedly doesn't handle UNC paths correctly,
// and anyway we need more info than it provides. Also note that it is possible
// for a file to begin with space(s) or a dot (if created programmatically), so
// don't trim or omit leading space unless it's known to be an absolute path.
// Note that "C:Some File.txt" is a valid filename in some contexts, which the below
// tries to take into account. However, there will be no way for this command to
// return a path that differentiates between "C:Some File.txt" and "C:\Some File.txt"
// since the first backslash is not included with the returned path, even if it's
// the root directory (i.e. "C:" is returned in both cases). The "C:Filename"
// convention is pretty rare, and anyway this trait can be detected via something like
// IfInString, Filespec, :, IfNotInString, Filespec, :\, MsgBox Drive with no absolute path.
// UNCs are detected with this approach so that double sets of backslashes -- which sometimes
// occur by accident in "built filespecs" and are tolerated by the OS -- are not falsely
// detected as UNCs.
if (drive[0] == '\\' && drive[1] == '\\') // Relies on short-circuit evaluation order.
{
if ( !(drive_end = strchr(drive + 2, '\\')) )
drive_end = drive + strlen(drive); // Set it to the position of the zero terminator instead.
}
else if (*(drive + 1) == ':') // It's an absolute path.
// Assign letter and colon for consistency with server naming convention above.
// i.e. so that server name and drive can be used without having to worry about
// whether it needs a colon added or not.
drive_end = drive + 2;
else
{
// It's debatable, but it seems best to return a blank drive if a aFileSpec is a relative path.
// rather than trying to use GetFullPathName() on a potentially non-existent file/dir.
// _splitpath() doesn't fetch the drive letter of relative paths either. This also reports
// a blank drive for something like file://C:\My Folder\My File.txt, which seems too rarely
// to justify a special mode.
drive_end = "";
drive = ""; // This is necessary to allow Assign() to work correctly later below, since it interprets a length of zero as "use string's entire length".
}
if ( !(name_delimiter = strrchr(aFileSpec, '\\')) ) // No backslash.
if ( !(name_delimiter = strrchr(aFileSpec, ':')) ) // No colon.
name_delimiter = NULL; // Indicate that there is no directory.
name = name_delimiter ? name_delimiter + 1 : aFileSpec; // If no delimiter, name is the entire string.
}
// The above has now set the following variables:
// name: As an empty string or the actual name of the file, including extension.
// name_delimiter: As NULL if there is no directory, otherwise, the end of the directory's name.
// drive: As the start of the drive/server name, e.g. C:, \\Workstation01, http://domain.com, etc.
// drive_end: As the position after the drive's last character, either a zero terminator, slash, or backslash.
if (output_var_name && !output_var_name->Assign(name))
return FAIL;
if (output_var_dir)
{
if (!name_delimiter)
output_var_dir->Assign(); // Shouldn't fail.
else if (*name_delimiter == '\\' || *name_delimiter == '/')
{
if (!output_var_dir->Assign(aFileSpec, (VarSizeType)(name_delimiter - aFileSpec)))
return FAIL;
}
else // *name_delimiter == ':', e.g. "C:Some File.txt". If aFileSpec starts with just ":",
// the dir returned here will also start with just ":" since that's rare & illegal anyway.
if (!output_var_dir->Assign(aFileSpec, (VarSizeType)(name_delimiter - aFileSpec + 1)))
return FAIL;
}
char *ext_dot = strrchr(name, '.');
if (output_var_ext)
{
// Note that the OS doesn't allow filenames to end in a period.
if (!ext_dot)
output_var_ext->Assign();
else
if (!output_var_ext->Assign(ext_dot + 1)) // Can be empty string if filename ends in just a dot.
return FAIL;
}
if (output_var_name_no_ext && !output_var_name_no_ext->Assign(name, (VarSizeType)(ext_dot ? ext_dot - name : strlen(name))))
return FAIL;
if (output_var_drive && !output_var_drive->Assign(drive, (VarSizeType)(drive_end - drive)))
return FAIL;
return OK;
}
int SortWithOptions(const void *a1, const void *a2)
// Decided to just have one sort function since there are so many permutations. The performance
// will be a little bit worse, but it seems simpler to implement and maintain.
// This function's input parameters are pointers to the elements of the array. Snce those elements
// are themselves pointers, the input parameters are therefore pointers to pointers (handles).
{
char *sort_item1 = *(char **)a1;
char *sort_item2 = *(char **)a2;
if (g_SortColumnOffset > 0)
{
// Adjust each string (even for numerical sort) to be the right column position,
// or the position of its zero terminator if the column offset goes beyond its length:
// Need to check if backup of function's variables is needed in case:
// 1) The UDF is assigned to more than one callback, in which case the UDF could be running more than one
// simultantously.
// 2) The callback is intended to be reentrant (e.g. a subclass/WindowProc that doesn't Critical).
// 3) Script explicitly calls the UDF in addition to using it as a callback.
//
// See ExpandExpression() for detailed comments about the following section.
VarBkp *var_backup = NULL; // If needed, it will hold an array of VarBkp objects.
int var_backup_count; // The number of items in the above array.
if (g_SortFunc->mInstances > 0) // Backup is needed.
if (!Var::BackupFunctionVars(*g_SortFunc, var_backup, var_backup_count)) // Out of memory.
return 0; // Since out-of-memory is so rare, it seems justifiable not to have any error reporting and instead just say "these items are equal".
// The following isn't necessary because by definition, the current thread isn't paused because it's the
// thing that called the sort in the first place.
//g_script.UpdateTrayIcon();
char *return_value;
g_SortFunc->mParam[0].var->Assign(*(char **)a1); // For simplicity and due to extreme rarity, parameters beyond
g_SortFunc->mParam[1].var->Assign(*(char **)a2); // the first 2 aren't populated even if they have default values.
if (g_SortFunc->mParamCount > 2)
g_SortFunc->mParam[2].var->Assign((__int64)(*(char **)a2 - *(char **)a1)); // __int64 to allow for a list greater than 2 GB, though that is currently impossible.
g_SortFunc->Call(return_value); // Call the UDF.
// MUST handle return_value BEFORE calling FreeAndRestoreFunctionVars() because return_value might be
// the contents of one of the function's local variables (which are about to be free'd).
int returned_int;
if (*return_value) // No need to check the following because they're implied for *return_value!=0: result != EARLY_EXIT && result != FAIL;
{
// Using float vs. int makes sort up to 46% slower, so decided to use int. Must use ATOI64 vs. ATOI
// because otherwise a negative might overflow/wrap into a positive (at least with the MSVC++
if (i64 > 0) // Maybe there's a faster/better way to do these checks. Can't simply typecast to an int because some large positives wrap into negative, maybe vice versa.
// Caller must ensure that aContents is modifiable (ArgMustBeDereferenced() currently ensures this) because
// not only does this function modify it, it also needs to store its result back into output_var in a way
// that requires that output_var not be at the same address as the contents that were sorted.
// It seems best to treat ACT_SORT's var to be an input vs. output var because if
// it's an environment variable or the clipboard, the input variable handler will
// automatically resolve it to be ARG1 (i.e. put its contents into the deref buf).
// This is especially necessary if the clipboard contains files, in which case
// output_var->Get(), not Contents(), must be used to resolve the filenames into text.
// And on average, using the deref buffer for this operation will not be wasteful in
// terms of expanding it unnecessarily, because usually the contents will have been
// already (or will soon be) in the deref buffer as a result of commands before
// or after the Sort command in the script.
{
// Set defaults in case of early goto:
char *mem_to_free = NULL;
Func *sort_func_orig = g_SortFunc; // Because UDFs can be interrupted by other threads -- and because UDFs can themselves call Sort with some other UDF (unlikely to be sure) -- backup & restore original g_SortFunc so that the "collapsing in reverse order" behavior will automatically ensure proper operation.
g_SortFunc = NULL; // Now that original has been saved above, reset to detect whether THIS sort uses a UDF.
ResultType result_to_return = OK;
DWORD ErrorLevel = -1; // Use -1 to mean "don't change/set ErrorLevel".
// Resolve options. First set defaults for options:
if (toupper(cp[1]) == 'L') // v1.0.43.03: Locale-insensitive mode, which probably performs considerably worse.
{
++cp;
g_SortCaseSensitive = SCS_INSENSITIVE_LOCALE;
}
else
g_SortCaseSensitive = SCS_SENSITIVE;
break;
case 'D':
if (!cp[1]) // Avoids out-of-bounds when the loop's own ++cp is done.
break;
++cp;
if (*cp)
delimiter = *cp;
break;
case 'F': // v1.0.47: Support a callback function to extend flexibility.
// Decided not to set ErrorLevel here because omit-dupes already uses it, and the code/docs
// complexity of having one take precedence over the other didn't seem worth it given rarity
// of errors and rarity of UDF use.
cp = omit_leading_whitespace(cp + 1); // Point it to the function's name.
if ( !(cp_end = StrChrAny(cp, " \t")) ) // Find space or tab, if any.
cp_end = cp + strlen(cp); // Point it to the terminator instead.
if ( !(g_SortFunc = g_script.FindFunc(cp, cp_end - cp)) )
goto end; // For simplicity, just abort the sort.
// To improve callback performance, ensure there are no ByRef parameters (for simplicity:
// not even ones that have default values) among the first two parameters. This avoids the
// need to ensure formal parameters are non-aliases each time the callback is called.
if (g_SortFunc->mIsBuiltIn || g_SortFunc->mParamCount < 2 // This validation is relied upon at a later stage.
|| g_SortFunc->mParamCount > 3 // Reserve 4-or-more parameters for possible future use (to avoid breaking existing scripts if such features are ever added).
|| g_SortFunc->mParam[0].is_byref || g_SortFunc->mParam[1].is_byref) // Relies on short-circuit boolean order.
goto end; // For simplicity, just abort the sort.
// Otherwise, the function meets the minimum contraints (though for simplicity, optional parameters
// (default values), if any, aren't populated).
// Fix for v1.0.47.05: The following line now subtracts 1 in case *cp_end=='\0'; otherwise the
// loop's ++cp would go beyond the terminator when there are no more options.
cp = cp_end - 1; // In the next interation (which also does a ++cp), resume looking for options after the function's name.
break;
case 'N':
g_SortNumeric = true;
break;
case 'P':
// Use atoi() vs. ATOI() to avoid interpreting something like 0x01C as hex
// when in fact the C was meant to be an option letter:
g_SortColumnOffset = atoi(cp + 1);
if (g_SortColumnOffset < 1)
g_SortColumnOffset = 1;
--g_SortColumnOffset; // Convert to zero-based.
break;
case 'R':
if (!strnicmp(cp, "Random", 6))
{
sort_random = true;
cp += 5; // Point it to the last char so that the loop's ++cp will point to the character after it.
}
else
g_SortReverse = true;
break;
case 'U': // Unique.
omit_dupes = true;
ErrorLevel = 0; // Set default dupe-count to 0 in case of early return.
break;
case 'Z':
// By setting this to true, the final item in the list, if it ends in a delimiter,
// Check for early return only after parsing options in case an option that sets ErrorLevel is present:
if (!*aContents) // Variable is empty, nothing to sort.
goto end;
Var &output_var = *OUTPUT_VAR; // The input var (ARG1) is also the output var in this case.
// Do nothing for reserved variables, since most of them are read-only and besides, none
// of them (realistically) should ever need sorting:
if (VAR_IS_READONLY(output_var)) // output_var can be a reserved variable because it's not marked as an output-var by ArgIsVar() [since it has a dual purpose as an input-var].
goto end;
// size_t helps performance and should be plenty of capacity for many years of advancement.
// In addition, things like realloc() can't accept anything larger than size_t anyway,
// so there's no point making this 64-bit until size_t itself becomes 64-bit (it already is on some compilers?).
size_t item_count;
// Explicitly calculate the length in case it's the clipboard or an environment var.
// (in which case Length() does not contain the current length). While calculating
// the length, also check how many delimiters are present:
for (item_count = 1, cp = aContents; *cp; ++cp) // Start at 1 since item_count is delimiter_count+1
if (*cp == delimiter)
++item_count;
size_t aContents_length = cp - aContents;
// If the last character in the unsorted list is a delimiter then technically the final item
// in the list is a blank item. However, if the options specify not to allow that, don't count that
// blank item as an actual item (i.e. omit it from the list):
if (!trailing_delimiter_indicates_trailing_blank_item && cp > aContents && cp[-1] == delimiter)
{
terminate_last_item_with_delimiter = true; // Have a later stage add the delimiter to the end of the sorted list so that the length and format of the sorted list is the same as the unsorted list.
--item_count; // Indicate that the blank item at the end of the list isn't being counted as an item.
}
else // The final character isn't a delimiter or it is but there's a blank item to its right. Either way, the following is necessary (veriifed correct).
if (first_delimiter && first_delimiter > aContents && first_delimiter[-1] == '\r')
{
// v1.0.47.05:
// Here the delimiter is effectively CRLF vs. LF. Therefore, signal a later section to append
// an extra CRLF to simplify the code and fix bugs that existed prior to v1.0.47.05.
// One such bug is that sorting "x`r`nx" in unique mode would previously produce two
// x's rather than one because the first x is seen to have a `r to its right but the
// second lacks it (due to the CRLF delimiter being simulated via LF-only).
//
// OLD/OBSOLETE comment from a section that was removed because it's now handled by this section:
// Check if special handling is needed due to the following situation:
// Delimiter is LF but the contents are lines delimited by CRLF, not just LF
// and the original/unsorted list's last item was not terminated by an
// "allowed delimiter". The symptoms of this are that after the sort, the
// last item will end in \r when it should end in no delimiter at all.
// This happens pretty often, such as when the clipboard contains files.
// In the future, an option letter can be added to turn off this workaround,
// but it seems unlikely that anyone would ever want to.
trailing_crlf_added_temporarily = true;
terminate_last_item_with_delimiter = true; // This is done to "fool" later sections into thinking the list ends in a CRLF that doesn't have a blank item to the right, which in turn simplifies the logic and/or makes it more understandable.
}
}
}
if (item_count == 1) // 1 item is already sorted, and no dupes are possible.
{
// Put the exact contents back into the output_var, which is necessary in case
// the variable was an environment variable or the clipboard-containing-files,
// since in those cases we want the behavior to be consistent regardless of
// whether there's only 1 item or more than one:
// Clipboard-contains-files: The single file should be translated into its
// text equivalent. Var was an environment variable: the corresponding script
// variable should be assigned the contents, so it will basically "take over"
// v1.0.47.05: It simplifies the code a lot to allocate and/or improves understandability to allocate
// memory for trailing_crlf_added_temporarily even though technically it's done only to make room to
// append the extra CRLF at the end.
if (g_SortFunc || trailing_crlf_added_temporarily) // Do this here rather than earlier with the options parsing in case the function-option is present twice (unlikely, but it would be a memory leak due to strdup below). Doing it here also avoids allocating if it isn't necessary.
{
// When g_SortFunc!=NULL, the copy of the string is needed because an earlier stage has ensured that
// aContents is in the deref buffer, but that deref buffer is about to be overwritten by the
// execution of the script's UDF body.
if ( !(mem_to_free = (char *)malloc(aContents_length + 3)) ) // +1 for terminator and +2 in case of trailing_crlf_added_temporarily.
{
result_to_return = LineError(ERR_OUTOFMEM); // Short msg. since so rare.
goto end;
}
memcpy(mem_to_free, aContents, aContents_length + 1); // memcpy() usually benches a little faster than strcpy().
aContents = mem_to_free;
if (trailing_crlf_added_temporarily)
{
// Must be added early so that the next stage will terminate at the \n, leaving the \r as
// the ending character in this item.
strcpy(aContents + aContents_length, "\r\n");
aContents_length += 2;
}
}
// Create the array of pointers that points into aContents to each delimited item.
// Use item_count + 1 to allow space for the last (blank) item in case
// trailing_delimiter_indicates_trailing_blank_item is false:
if (*cp == delimiter) // Each delimiter char becomes the terminator of the previous key phrase.
{
*cp = '\0'; // Terminate the item that appears before this delimiter.
++item_count;
if (sort_random)
*(item_curr + 1) = (char *)(size_t)genrand_int31(); // i.e. the randoms are in the odd fields, the pointers in the even.
// For the above:
// I don't know the exact reasons, but using genrand_int31() is much more random than
// using genrand_int32() in this case. Perhaps it is some kind of statistical/cyclical
// anomaly in the random number generator. Or perhaps it's something to do with integer
// underflow/overflow in SortRandom(). In any case, the problem can be proven via the
// following script, which shows a sharply non-random distribution when genrand_int32()
// is used:
//count = 0
//Loop 10000
//{
// var = 1`n2`n3`n4`n5`n
// Sort, Var, Random
// StringLeft, Var1, Var, 1
// if Var1 = 5 ; Change this value to 1 to see the opposite problem.
// count += 1
//}
//Msgbox %count%
//
// I e-mailed the author about this sometime around/prior to 12/1/04 but never got a response.
item_curr += unit_size; // i.e. Don't use [] indexing for the reason described above.
*item_curr = cp + 1; // Make a pointer to the next item's place in aContents.
}
}
// The above reset the count to 0 and recounted it. So now re-add the last item to the count unless it was
// disqualified earlier. Verified correct:
if (!terminate_last_item_with_delimiter) // i.e. either trailing_delimiter_indicates_trailing_blank_item==true OR the final character isn't a delimiter. Either way the final item needs to be added.
{
++item_count;
if (sort_random) // Provide a random number for the last item.
*(item_curr + 1) = (char *)(size_t)genrand_int31(); // i.e. the randoms are in the odd fields, the pointers in the even.
}
else // Since the final item is not included in the count, point item_curr to the one before the last, for use below.
item_curr -= unit_size;
// Now aContents has been divided up based on delimiter. Sort the array of pointers
// so that they indicate the correct ordering to copy aContents into output_var:
if (g_SortFunc) // Takes precedence other sorting methods.
// Copy the sorted pointers back into output_var, which might not already be sized correctly
// if it's the clipboard or it was an environment variable when it came in as the input.
// If output_var is the clipboard, this call will set up the clipboard for writing:
if (output_var.Assign(NULL, (VarSizeType)aContents_length) != OK) // Might fail due to clipboard problem.
{
result_to_return = FAIL;
goto end;
}
// Set default in case original last item is still the last item, or if last item was omitted due to being a dupe:
size_t i, item_count_minus_1 = item_count - 1;
DWORD omit_dupe_count = 0;
bool keep_this_item;
char *source, *dest;
char *item_prev = NULL;
// Copy the sorted result back into output_var. Do all except the last item, since the last
// item gets special treatment depending on the options that were specified. The call to
// output_var->Contents() below should never fail due to the above having prepped it:
item_curr = item; // i.e. Don't use [] indexing for the reason described higher above (same applies to item += unit_size below).
for (dest = output_var.Contents(), i = 0; i < item_count; ++i, item_curr += unit_size)
{
keep_this_item = true; // Set default.
if (omit_dupes && item_prev)
{
// Update to the comment below: Exact dupes will still be removed when sort_by_naked_filename
// or g_SortColumnOffset is in effect because duplicate lines would still be adjacent to
// each other even in these modes. There doesn't appear to be any exceptions, even if
// some items in the list are sorted as blanks due to being shorter than the specified
// g_SortColumnOffset.
// As documented, special dupe-checking modes are not offered when sort_by_naked_filename
// is in effect, or g_SortColumnOffset is greater than 1. That's because the need for such
// a thing seems too rare (and the result too strange) to justify the extra code size.
// However, adjacent dupes are still removed when any of the above modes are in effect,
// or when the "random" mode is in effect. This might have some usefulness; for example,
// if a list of songs is sorted in random order, but it contains "favorite" songs listed twice,
// the dupe-removal feature would remove duplicate songs if they happen to be sorted
// to lie adjacent to each other, which would be useful to prevent the same song from
// playing twice in a row.
if (g_SortNumeric && !g_SortColumnOffset)
// if g_SortColumnOffset is zero, fall back to the normal dupe checking in case its
// ever useful to anyone. This is done because numbers in an offset column are not supported
// since the extra code size doensn't seem justified given the rarity of the need.
keep_this_item = (ATOF(*item_curr) != ATOF(item_prev)); // ATOF() ignores any trailing \r in CRLF mode, so no extra logic is needed for that.
else
keep_this_item = strcmp2(*item_curr, item_prev, g_SortCaseSensitive); // v1.0.43.03: Added support for locale-insensitive mode.
// Permutations of sorting case sensitive vs. eliminating duplicates based on case sensitivity:
// 1) Sort is not case sens, but dupes are: Won't work because sort didn't necessarily put
// same-case dupes adjacent to each other.
// 2) Converse: probably not reliable because there could be unrelated items in between
// two strings that are duplicates but weren't sorted adjacently due to their case.
// 3) Both are case sensitive: seems okay
// 4) Both are not case sensitive: seems okay
//
// In light of the above, using the g_SortCaseSensitive flag to control the behavior of
// both sorting and dupe-removal seems best.
}
if (keep_this_item)
{
for (source = *item_curr; *source;)
*dest++ = *source++;
// If we're at the last item and the original list's last item had a terminating delimiter
// and the specified options said to treat it not as a delimiter but as a final char of sorts,
// include it after the item that is now last so that the overall layout is the same:
if (i < item_count_minus_1 || terminate_last_item_with_delimiter)
*dest++ = delimiter; // Put each item's delimiter back in so that format is the same as the original.
item_prev = *item_curr; // Since the item just processed above isn't a dupe, save this item to compare against the next item.
}
else // This item is a duplicate of the previous item.
{
++omit_dupe_count; // But don't change the value of item_prev.
// v1.0.47.05: The following section fixes the fact that the "unique" option would sometimes leave
// a trailing delimiter at the end of the sorted list. For example, sorting "x|x" in unique mode
// would formerly produce "x|":
if (i == item_count_minus_1 && !terminate_last_item_with_delimiter) // This is the last item and its being omitted, so remove the previous item's trailing delimiter (unless a trailing delimiter is mandatory).
--dest; // Remove the previous item's trailing delimiter there's nothing for it to delimit due to omission of this duplicate.
}
} // for()
free(item); // Free the index/pointer list used for the sort.
// Terminate the variable's contents.
if (trailing_crlf_added_temporarily) // Remove the CRLF only after its presence was used above to simplify the code by reducing the number of types/cases.
{
dest[-2] = '\0';
output_var.Length() -= 2;
}
else
*dest = '\0';
if (omit_dupes)
{
if (omit_dupe_count) // Update the length to actual whenever at least one dupe was omitted.
// Sleep a little longer than normal because I'm not sure how much overhead
// and CPU utilization the above incurs:
MsgSleep(20);
}
return OK;
}
void SetWorkingDir(char *aNewDir)
// Sets ErrorLevel to indicate success/failure, but only if the script has begun runtime execution (callers
// want that).
// This function was added in v1.0.45.01 for the reasons commented further below.
// It's similar to the one in the ahk2exe source, so maintain them together.
{
if (!SetCurrentDirectory(aNewDir)) // Caused by nonexistent directory, permission denied, etc.
{
if (g_script.mIsReadyToExecute)
g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
return;
}
// Otherwise, the change to the working directory *apparently* succeeded (but is confirmed below for root drives
// and also because we want the absolute path in cases where aNewDir is relative).
char buf[sizeof(g_WorkingDir)];
char *actual_working_dir = g_script.mIsReadyToExecute ? g_WorkingDir : buf; // i.e. don't update g_WorkingDir when our caller is the #include directive.
// Other than during program startup, this should be the only place where the official
// working dir can change. The exception is FileSelectFile(), which changes the working
// dir as the user navigates from folder to folder. However, the whole purpose of
// maintaining g_WorkingDir is to workaround that very issue.
// GetCurrentDirectory() is called explicitly, to confirm the change, in case aNewDir is a relative path.
// We want to store the absolute path:
if (!GetCurrentDirectory(sizeof(buf), actual_working_dir)) // Might never fail in this case, but kept for backward compatibility.
{
strlcpy(actual_working_dir, aNewDir, sizeof(buf)); // Update the global to the best info available.
// But ErrorLevel is set to "none" further below because the actual "set" did succeed; it's also for
// backward compatibility.
}
else // GetCurrentDirectory() succeeded, so it's appropriate to compare what we asked for to what was received.
{
if (aNewDir[0] && aNewDir[1] == ':' && !aNewDir[2] // Root with missing backslash. Relies on short-circuit boolean order.
&& stricmp(aNewDir, actual_working_dir)) // The root directory we requested didn't actually get set. See below.
{
// There is some strange OS behavior here: If the current working directory is C:\anything\...
// and SetCurrentDirectory() is called to switch to "C:", the function reports success but doesn't
// actually change the directory. However, if you first change to D: then back to C:, the change
// works as expected. Presumably this is for backward compatibility with DOS days; but it's
// inconvenient and seems desirable to override it in this case, especially because:
// v1.0.45.01: Since A_ScriptDir omits the trailing backslash for roots of drives (such as C:),
// and since that variable probably shouldn't be changed for backward compatibility, provide
// the missing backslash to allow SetWorkingDir %A_ScriptDir% (and others) to work in the root
// of a drive.
char buf_temp[8];
sprintf(buf_temp, "%s\\", aNewDir); // No danger of buffer overflow in this case.
if (SetCurrentDirectory(buf_temp))
{
if (!GetCurrentDirectory(sizeof(buf), actual_working_dir)) // Might never fail in this case, but kept for backward compatibility.
strlcpy(actual_working_dir, aNewDir, sizeof(buf)); // But still report "no error" (below) because the Set() actually did succeed.
// But treat this as a success like the similar one higher above.
}
//else Set() failed; but since the origial Set() succeeded (and for simplicity) report ErrorLevel "none".
}
}
// Since the above didn't return, it wants us to indicate success.
if (g_script.mIsReadyToExecute) // Callers want ErrorLevel changed only during script runtime.
, aFilter, '\0', pattern, '\0', '\0', '\0'); // The final '\0' double-terminates by virtue of the fact that snprintf() itself provides a final terminator.
}
else
*filter = '\0'; // It will use a standard default below.
}
OPENFILENAME ofn = {0};
// OPENFILENAME_SIZE_VERSION_400 must be used for 9x/NT otherwise the dialog will not appear!
// MSDN: "In an application that is compiled with WINVER and _WIN32_WINNT >= 0x0500, use
// OPENFILENAME_SIZE_VERSION_400 for this member. Windows 2000/XP: Use sizeof(OPENFILENAME)
ofn.hwndOwner = THREAD_DIALOG_OWNER; // Can be NULL, which is used instead of main window since no need to have main window forced into the background for this.
if (bytes_to_read == ULLONG_MAX // GetFileSize64() failed...
|| max_bytes_to_load == ULLONG_MAX && bytes_to_read > FILEREAD_MAX) // ...or the file is too large to be completely read (and the script wanted it completely read).
{
CloseHandle(hfile);
return OK; // Let ErrorLevel tell the story.
}
if (max_bytes_to_load < bytes_to_read)
bytes_to_read = max_bytes_to_load;
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Set default for this point forward to be "success".
if (!bytes_to_read)
{
CloseHandle(hfile);
return OK; // And ErrorLevel will indicate success (a zero-length file results in empty output_var).
}
// Set up the var, enlarging it if necessary. If the output_var is of type VAR_CLIPBOARD,
// this call will set up the clipboard for writing:
if (output_var.Assign(NULL, (VarSizeType)bytes_to_read, true, false) != OK) // Probably due to "out of memory".
{
CloseHandle(hfile);
return FAIL; // It already displayed the error. ErrorLevel doesn't matter now because the current quasi-thread will be aborted.
}
char *output_buf = output_var.Contents();
DWORD bytes_actually_read;
BOOL result = ReadFile(hfile, output_buf, (DWORD)bytes_to_read, &bytes_actually_read, NULL);
CloseHandle(hfile);
// Upon result==success, bytes_actually_read is not checked against bytes_to_read because it
// shouldn't be different (result should have set to failure if there was a read error).
// If it ever is different, a partial read is considered a success since ReadFile() told us
// that nothing bad happened.
if (result)
{
output_buf[bytes_actually_read] = '\0'; // Ensure text is terminated where indicated.
// Since a larger string is being replaced with a smaller, there's a good chance the 2 GB
// address limit will not be exceeded by StrReplace even if the file is close to the
// 1 GB limit as described above:
if (translate_crlf_to_lf)
StrReplace(output_buf, "\r\n", "\n", SCS_SENSITIVE); // Safe only because larger string is being replaced with smaller.
output_var.Length() = is_binary_clipboard ? (bytes_actually_read - 1) // Length excludes the very last byte of the (UINT)0 terminator.
: (VarSizeType)strlen(output_buf); // In case file contains binary zeroes, explicitly calculate the "usable" length so that it's accurate.
}
else
{
// ReadFile() failed. Since MSDN does not document what is in the buffer at this stage,
// or whether what's in it is even null-terminated, or even whether bytes_to_read contains
// a valid value, it seems best to abort the entire operation rather than try to put partial
// file contents into output_var. ErrorLevel will indicate the failure.
// Since ReadFile() failed, to avoid complications or side-effects in functions such as Var::Close(),
// avoid storing a potentially non-terminated string in the variable.
*output_buf = '\0';
output_var.Length() = 0;
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Override the success default that was set in the middle of this function.
}
// ErrorLevel, as set in various places above, indicates success or failure.
// Returns OK or FAIL. Will almost always return OK because if an error occurs,
// the script's ErrorLevel variable will be set accordingly. However, if some
// kind of unexpected and more serious error occurs, such as variable-out-of-memory,
// that will cause FAIL to be returned.
{
Var &output_var = *OUTPUT_VAR; // Fix for v1.0.45.01: Must be resolved and saved before MsgSleep() (LONG_OPERATION) because that allows some other thread to interrupt and overwrite sArgVar[].
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Set default ErrorLevel.
__int64 line_number = ATOI64(aLineNumber);
if (line_number < 1)
return OK; // Return OK because g_ErrorLevel tells the story.
FILE *fp = fopen(aFilespec, "r");
if (!fp)
return OK; // Return OK because g_ErrorLevel tells the story.
// Remember that once the first call to MsgSleep() is done, a new hotkey subroutine
// may fire and suspend what we're doing here. Such a subroutine might also overwrite
// the values our params, some of which may be in the deref buffer. So be sure not
// to refer to those strings once MsgSleep() has been done, below. Alternatively,
// a copy of such params can be made using our own stack space.
LONG_OPERATION_INIT
char buf[READ_FILE_LINE_SIZE];
for (__int64 i = 0; i < line_number; ++i)
{
if (fgets(buf, sizeof(buf) - 1, fp) == NULL) // end-of-file or error
{
fclose(fp);
return OK; // Return OK because g_ErrorLevel tells the story.
}
LONG_OPERATION_UPDATE
}
fclose(fp);
size_t buf_length = strlen(buf);
if (buf_length && buf[buf_length - 1] == '\n') // Remove any trailing newline for the user.
buf[--buf_length] = '\0';
if (!buf_length)
{
if (!output_var.Assign()) // Explicitly call it this way so that it won't free the memory.
return FAIL;
}
else
if (!output_var.Assign(buf, (VarSizeType)buf_length))
// Returns OK or FAIL. If OK, it sets ErrorLevel to the appropriate result.
// If the clipboard is empty, a zero length file will be written, which seems best for its consistency.
{
// This method used is very similar to that used in PerformAssign(), so see that section
// for a large quantity of comments.
if (!g_clip.Open())
return LineError(CANT_OPEN_CLIPBOARD_READ); // Make this a critical/stop error since it's unusual and something the user would probably want to know about.
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Set default.
HANDLE hfile = CreateFile(aFilespec, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); // Overwrite. Unsharable (since reading the file while it is being written would probably produce bad data in this case).
if (hfile == INVALID_HANDLE_VALUE)
return g_clip.Close(); // Let ErrorLevel tell the story.
GlobalUnlock(hglobal); // hglobal not hglobal_locked.
break; // File might be in an incomplete state now, but that's okay because the reading process checks for that.
}
if (size)
{
result = WriteFile(hfile, hglobal_locked, (DWORD)size, &bytes_written, NULL);
GlobalUnlock(hglobal); // hglobal not hglobal_locked.
if (!result)
break; // File might be in an incomplete state now, but that's okay because the reading process checks for that.
}
//else hglobal_locked is not valid, so don't reference it or unlock it. In other words, 0 bytes are written for this format.
}
}
g_clip.Close();
if (!format) // Since the loop was not terminated as a result of a failed WriteFile(), write the 4-byte terminator (otherwise, omit it to avoid further corrupting the file).
if (WriteFile(hfile, &format, sizeof(format), &bytes_written, NULL))
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // Indicate success (otherwise, leave it set to failure).
CloseHandle(hfile);
return OK; // Let ErrorLevel, set above, tell the story.
// Returns OK or FAIL. If OK, ErrorLevel is overridden from the callers ERRORLEVEL_ERROR setting to
// ERRORLEVEL_SUCCESS, if appropriate. This function also closes hfile before returning.
// The method used here is very similar to that used in PerformAssign(), so see that section
// for a large quantity of comments.
{
if (!g_clip.Open())
{
CloseHandle(hfile);
return LineError(CANT_OPEN_CLIPBOARD_WRITE); // Make this a critical/stop error since it's unusual and something the user would probably want to know about.
}
EmptyClipboard(); // Failure is not checked for since it's probably impossible under these conditions.
UINT format;
HGLOBAL hglobal;
LPVOID hglobal_locked;
SIZE_T size;
DWORD bytes_read;
if (!ReadFile(hfile, &format, sizeof(format), &bytes_read, NULL) || bytes_read < sizeof(format))
{
g_clip.Close();
CloseHandle(hfile);
return OK; // Let ErrorLevel, set by the caller, tell the story.
}
while (format)
{
if (!ReadFile(hfile, &size, sizeof(size), &bytes_read, NULL) || bytes_read < sizeof(size))
break; // Leave what's already on the clipboard intact since it might be better than nothing.
if ( !(hglobal = GlobalAlloc(GMEM_MOVEABLE, size)) ) // size==0 is okay.
{
g_clip.Close();
CloseHandle(hfile);
return LineError(ERR_OUTOFMEM); // Short msg since so rare.
}
if (size) // i.e. Don't try to lock memory of size zero. It won't work and it's not needed.
{
if ( !(hglobal_locked = GlobalLock(hglobal)) )
{
GlobalFree(hglobal);
g_clip.Close();
CloseHandle(hfile);
return LineError("GlobalLock"); // Short msg since so rare.
}
if (!ReadFile(hfile, hglobal_locked, (DWORD)size, &bytes_read, NULL) || bytes_read < size)
{
GlobalUnlock(hglobal);
GlobalFree(hglobal); // Seems best not to do SetClipboardData for incomplete format (especially without zeroing the unused portion of global_locked).
break; // Leave what's already on the clipboard intact since it might be better than nothing.
}
GlobalUnlock(hglobal);
}
//else hglobal is just an empty format, but store it for completeness/accuracy (e.g. CF_BITMAP).
SetClipboardData(format, hglobal); // The system now owns hglobal.
if (!ReadFile(hfile, &format, sizeof(format), &bytes_read, NULL) || bytes_read < sizeof(format))
break;
}
g_clip.Close();
CloseHandle(hfile);
if (format) // The loop ended as a result of a file error.
return OK; // Let ErrorLevel, set above, tell the story.
else // Indicate success.
return g_ErrorLevel->Assign(ERRORLEVEL_NONE);
}
ResultType Line::FileDelete()
{
// Below is done directly this way rather than passed in as args mainly to emphasize that
// ArgLength() can safely be called in Line methods like this one (which is done further below).
// It also may also slightly improve performance and reduce code size.
char *aFilePattern = ARG1;
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Set default ErrorLevel, namely that "one file couldn't be deleted".
if (!*aFilePattern)
return OK; // Let ErrorLevel indicate an error, since this is probably not what the user intended.
return OK; // ErrorLevel will indicate failure if the above didn't succeed.
}
// Otherwise aFilePattern contains wildcards, so we'll search for all matches and delete them.
// Testing shows that the ANSI version of FindFirstFile() will not accept a path+pattern longer
// than 256 or so, even if the pattern would match files whose names are short enough to be legal.
// Therefore, as of v1.0.25, there is also a hard limit of MAX_PATH on all these variables.
// MSDN confirms this in a vague way: "In the ANSI version of FindFirstFile(), [plpFileName] is
// limited to MAX_PATH characters."
if (ArgLength(1) >= MAX_PATH) // Checked early to simplify things later below.
return OK; // Return OK because this is non-critical. Due to rarity (and backward compatibility), it seems best leave ErrorLevel at 1 to indicate the problem
if (file_search == INVALID_HANDLE_VALUE) // No matching files found.
return g_ErrorLevel->Assign(0); // Deleting a wildcard pattern that matches zero files is a success.
// Otherwise:
char file_path[MAX_PATH];
strcpy(file_path, aFilePattern); // Above has already confirmed this won't overflow.
// Remove the filename and/or wildcard part. But leave the trailing backslash on it for
// consistency with below:
size_t file_path_length;
char *last_backslash = strrchr(file_path, '\\');
if (last_backslash)
{
*(last_backslash + 1) = '\0'; // i.e. retain the trailing backslash.
file_path_length = strlen(file_path);
}
else // Use current working directory, e.g. if user specified only *.*
{
*file_path = '\0';
file_path_length = 0;
}
char *append_pos = file_path + file_path_length; // For performance, copy in the unchanging part only once. This is where the changing part gets appended.
size_t space_remaining = sizeof(file_path) - file_path_length - 1; // Space left in file_path for the changing part.
do
{
// Since other script threads can interrupt during LONG_OPERATION_UPDATE, it's important that
// this command not refer to sArgDeref[] and sArgVar[] anytime after an interruption becomes
// possible. This is because an interrupting thread usually changes the values to something
// inappropriate for this thread.
LONG_OPERATION_UPDATE
if (current_file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) // skip any matching directories.
continue;
if (strlen(current_file.cFileName) > space_remaining)
{
// v1.0.45.03: Don't even try to operate upon truncated filenames in case they accidentally
// match the name of a real/existing file.
++failure_count;
}
else
{
strcpy(append_pos, current_file.cFileName); // Above has ensured this won't overflow.
if (!DeleteFile(file_path))
++failure_count;
}
} while (FindNextFile(file_search, ¤t_file));
FindClose(file_search);
return g_ErrorLevel->Assign(failure_count); // i.e. indicate success if there were no failures.
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Set default ErrorLevel.
bool allow_overwrite = (ATOI(aFlag) == 1);
#ifdef AUTOHOTKEYSC
if (!allow_overwrite && Util_DoesFileExist(aDest))
return OK; // Let ErrorLevel tell the story.
HS_EXEArc_Read oRead;
// AutoIt3: Open the archive in this compiled exe.
// Jon gave me some details about why a password isn't needed: "The code in those libararies will
// only allow files to be extracted from the exe is is bound to (i.e the script that it was
// compiled with). There are various checks and CRCs to make sure that it can't be used to read
// the files from any other exe that is passed."
if (oRead.Open(g_script.mFileSpec, "") != HS_EXEARC_E_OK)
{
MsgBox(ERR_EXE_CORRUPTED, 0, g_script.mFileSpec); // Usually caused by virus corruption. Probably impossible since it was previously opened successfully to run the main script.
return OK; // Let ErrorLevel tell the story.
}
// aSource should be the same as the "file id" used to originally compress the file
// when it was compiled into an EXE. So this should seek for the right file:
int result = oRead.FileExtract(aSource, aDest);
oRead.Close();
// v1.0.46.15: The following is a fix for the fact that a compiled script (but not an uncompiled one)
// that executes FileInstall somehow causes the Random command to generate the same series of random
// numbers every time the script launches. Perhaps the answer lies somewhere in oRead's code --
// something that somehow resets the static data used by init_genrand().
// Testing shows that the ANSI version of FindFirstFile() will not accept a path+pattern longer
// than 256 or so, even if the pattern would match files whose names are short enough to be legal.
// Therefore, as of v1.0.25, there is also a hard limit of MAX_PATH on all these variables.
// MSDN confirms this in a vague way: "In the ANSI version of FindFirstFile(), [plpFileName] is
// limited to MAX_PATH characters."
char file_pattern[MAX_PATH], file_path[MAX_PATH]; // Giving +3 extra for "*.*" seems fairly pointless because any files that actually need that extra room would fail to be retrieved by FindFirst/Next due to their inability to support paths much over 256.
strcpy(file_pattern, aFilePattern); // Make a copy in case of overwrite of deref buf during LONG_OPERATION/MsgSleep.
strcpy(file_path, aFilePattern); // An earlier check has ensured these won't overflow.
size_t file_path_length; // The length of just the path portion of the filespec.
char *last_backslash = strrchr(file_path, '\\');
if (last_backslash)
{
// Remove the filename and/or wildcard part. But leave the trailing backslash on it for
// consistency with below:
*(last_backslash + 1) = '\0';
file_path_length = strlen(file_path);
}
else // Use current working directory, e.g. if user specified only *.*
{
*file_path = '\0';
file_path_length = 0;
}
char *append_pos = file_path + file_path_length; // For performance, copy in the unchanging part only once. This is where the changing part gets appended.
size_t space_remaining = sizeof(file_path) - file_path_length - 1; // Space left in file_path for the changing part.
// For use with aDoRecurse, get just the naked file name/pattern:
if (!SetFileAttributes(file_path, current_file.dwFileAttributes))
++failure_count;
} while (FindNextFile(file_search, ¤t_file));
FindClose(file_search);
} // if (file_search != INVALID_HANDLE_VALUE)
if (aDoRecurse && space_remaining > 2) // The space_remaining check ensures there's enough room to append "*.*" (if not, just avoid recursing into it due to rarity).
{
// Testing shows that the ANSI version of FindFirstFile() will not accept a path+pattern longer
// than 256 or so, even if the pattern would match files whose names are short enough to be legal.
// Therefore, as of v1.0.25, there is also a hard limit of MAX_PATH on all these variables.
// MSDN confirms this in a vague way: "In the ANSI version of FindFirstFile(), [plpFileName] is
// limited to MAX_PATH characters."
strcpy(append_pos, "*.*"); // Above has ensured this won't overflow.
// v1.0.45.03: Skip over folders whose full-path-names are too long to be supported by the ANSI
// versions of FindFirst/FindNext. Without this fix, it might be possible for infinite recursion
// to occur (see PerformLoop() for more comments).
|| pattern_length + strlen(current_file.cFileName) >= space_remaining) // >= vs. > to reserve 1 for the backslash to be added between cFileName and naked_filename_or_pattern.
continue; // Never recurse into these.
// This will build the string CurrentDir+SubDir+FilePatternOrName.
// If FilePatternOrName doesn't contain a wildcard, the recursion
// process will attempt to operate on the originally-specified
// single filename or folder name if it occurs anywhere else in the
// tree, e.g. recursing C:\Temp\temp.txt would affect all occurences
// of temp.txt both in C:\Temp and any subdirectories it might contain:
sprintf(append_pos, "%s\\%s" // Above has ensured this won't overflow.
// Returns the number of files and folders that could not be changed due to an error.
// Current limitation: It will not recurse into subfolders unless their names also match
// aFilePattern.
{
if (!aCalledRecursively) // i.e. Only need to do this if we're not called by ourself:
{
g_ErrorLevel->Assign(ERRORLEVEL_ERROR); // Set default
if (!*aFilePattern)
return 0; // Let ErrorLevel indicate an error, since this is probably not what the user intended.
if (aOperateOnFolders == FILE_LOOP_INVALID) // In case runtime dereference of a var was an invalid value.
aOperateOnFolders = FILE_LOOP_FILES_ONLY; // Set default.
}
if (strlen(aFilePattern) >= MAX_PATH) // Checked early to simplify other things below.
return 0; // Let the above ErrorLevel indicate the problem.
// Related to the comment at the top: Since the script subroutine that resulted in the call to
// this function can be interrupted during our MsgSleep(), make a copy of any params that might
// currently point directly to the deref buffer. This is done because their contents might
// be overwritten by the interrupting subroutine:
char yyyymmdd[64]; // Even do this one since its value is passed recursively in calls to self.
strlcpy(yyyymmdd, aYYYYMMDD, sizeof(yyyymmdd));
char file_pattern[MAX_PATH];
strcpy(file_pattern, aFilePattern); // An earlier check has ensured this won't overflow.
FILETIME ft, ftUTC;
if (*yyyymmdd)
{
// Convert the arg into the time struct as local (non-UTC) time:
if (!YYYYMMDDToFileTime(yyyymmdd, ft))
return 0; // Let ErrorLevel tell the story.
// Convert from local to UTC:
if (!LocalFileTimeToFileTime(&ft, &ftUTC))
return 0; // Let ErrorLevel tell the story.
}
else // User wants to use the current time (i.e. now) as the new timestamp.
GetSystemTimeAsFileTime(&ftUTC);
// This following section is very similar to that in FileSetAttrib and FileDelete:
char file_path[MAX_PATH]; // Giving +3 extra for "*.*" seems fairly pointless because any files that actually need that extra room would fail to be retrieved by FindFirst/Next due to their inability to support paths much over 256.
strcpy(file_path, aFilePattern); // An earlier check has ensured this won't overflow.
size_t file_path_length; // The length of just the path portion of the filespec.
char *last_backslash = strrchr(file_path, '\\');
if (last_backslash)
{
// Remove the filename and/or wildcard part. But leave the trailing backslash on it for
// consistency with below:
*(last_backslash + 1) = '\0';
file_path_length = strlen(file_path);
}
else // Use current working directory, e.g. if user specified only *.*
{
*file_path = '\0';
file_path_length = 0;
}
char *append_pos = file_path + file_path_length; // For performance, copy in the unchanging part only once. This is where the changing part gets appended.
size_t space_remaining = sizeof(file_path) - file_path_length - 1; // Space left in file_path for the changing part.
// For use with aDoRecurse, get just the naked file name/pattern:
default: // 'M', unspecified, or some other value. Use the file's modification time.
if (!SetFileTime(hFile, NULL, NULL, &ftUTC))
++failure_count;
}
CloseHandle(hFile);
} while (FindNextFile(file_search, ¤t_file));
FindClose(file_search);
} // if (file_search != INVALID_HANDLE_VALUE)
// This section is identical to that in FileSetAttrib() except for the recursive function
// call itself, so see comments there for details:
if (aDoRecurse && space_remaining > 2) // The space_remaining check ensures there's enough room to append "*.*" (if not, just avoid recursing into it due to rarity).
{
// Testing shows that the ANSI version of FindFirstFile() will not accept a path+pattern longer
// than 256 or so, even if the pattern would match files whose names are short enough to be legal.
// Therefore, as of v1.0.25, there is also a hard limit of MAX_PATH on all these variables.
// MSDN confirms this in a vague way: "In the ANSI version of FindFirstFile(), [plpFileName] is
// limited to MAX_PATH characters."
strcpy(append_pos, "*.*"); // Above has ensured this won't overflow.
// v1.0.45.03: Skip over folders whose full-path-names are too long to be supported by the ANSI
// versions of FindFirst/FindNext. Without this fix, it might be possible for infinite recursion
// to occur (see PerformLoop() for more comments).
|| pattern_length + strlen(current_file.cFileName) >= space_remaining) // >= vs. > to reserve 1 for the backslash to be added between cFileName and naked_filename_or_pattern.
continue;
sprintf(append_pos, "%s\\%s" // Above has ensured this won't overflow.
// v1.0.45.03: Filter out filenames that would be truncated because it seems undesirable in 99% of
// cases to include such "faulty" data in the loop. Most scripts would want to skip them rather than
// seeing the truncated names. Furthermore, a truncated name might accidentally match the name
// of a legitimate non-trucated filename, which could cause such a name to get retrieved twice by
// the loop (or other undesirable side-effects).
return true;
//else no overflow is possible, so below can move things around inside the buffer without concern.
memmove(aCurrentFile.cFileName + aFilePathLength, aCurrentFile.cFileName, name_length + 1); // memmove() because source & dest might overlap. +1 to include the terminator.
memcpy(aCurrentFile.cFileName, aFilePath, aFilePathLength); // Prepend in the area liberated by the above. Don't include the terminator since this is a concat operation.
}
return false; // Indicate that this file is not to be filtered out.
// Since aTargetLabel is in the same block as the Goto line itself (or a block
// that encloses that block), it's allowed:
return &aTargetLabel; // Indicate success.
// This can happen if the Goto's target is at a deeper level than it, or if the target
// is at a more shallow level but is in some block totally unrelated to it!
// Returns FAIL by default, which is what we want because that value is zero:
LineError("A Goto/Gosub must not jump into a block that doesn't enclose it."); // Omit GroupActivate from the error msg since that is rare enough to justify the increase in common-case clarify.
return NULL;
// Above currently doesn't attempt to detect runtime vs. load-time for the purpose of appending
? (VarSizeType)strlen(strcpy(aBuf, g.DetectHiddenWindows ? "On" : "Off")) // For backward compatibility (due to StringCaseSense), never change the case used here. Fixed in v1.0.42.01 to return exact length (required).
: 3; // Room for either On or Off (in the estimation phase).
? (VarSizeType)strlen(strcpy(aBuf, g.DetectHiddenText ? "On" : "Off")) // For backward compatibility (due to StringCaseSense), never change the case used here. Fixed in v1.0.42.01 to return exact length (required).
: 3; // Room for either On or Off (in the estimation phase).
? (VarSizeType)strlen(strcpy(aBuf, g.AutoTrim ? "On" : "Off")) // For backward compatibility (due to StringCaseSense), never change the case used here. Fixed in v1.0.42.01 to return exact length (required).
: 3; // Room for either On or Off (in the estimation phase).
? (VarSizeType)strlen(strcpy(aBuf, g.StringCaseSense == SCS_INSENSITIVE ? "Off" // For backward compatibility (due to StringCaseSense), never change the case used here. Fixed in v1.0.42.01 to return exact length (required).
, length + !(length && str_with_leading_percent_omitted[length-1] == 'f')); // Omit the trailing character only if it's an 'f', not any other letter such as the 'e' in "%0.6e" (for backward compatibility).
return (VarSizeType)strlen(aBuf); // Must return exact length when aBuf isn't NULL.
_itoa(g.DefaultMouseSpeed, target_buf, 10); // Always output as decimal vs. hex in this case (so that scripts can use "If var in list" with confidence).
// Name "AutoHotkey.exe" is assumed for code size reduction and because it's not stored in the registry:
strlcpy(aBuf + length, "\\AutoHotkey.exe", MAX_PATH - length); // strlcpy() in case registry has a path that is too close to MAX_PATH to fit AutoHotkey.exe
//else leave it blank as documented.
return (VarSizeType)strlen(aBuf);
}
// Otherwise: Always return an estimate of MAX_PATH in case the registry entry changes between the
// first call and the second. This is also relied upon by strlcpy() above, which zero-fills the tail
// of the destination up through the limit of its capacity (due to calling strncpy, which does this).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like this one.
memmove(buf, buf + 4, length + 1); // +1 to include the zero terminator.
}
}
if (aBuf)
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
char buf[MAX_PATH]; // Doesn't use MAX_COMPUTERNAME_LENGTH + 1 in case longer names are allowed in the future.
DWORD buf_size = MAX_PATH; // Below: A_Computer[N]ame (N is the 11th char, index 10, which if present at all distinguishes between the two).
if ( !(aVarName[10] ? GetComputerName(buf, &buf_size) : GetUserName(buf, &buf_size)) )
*buf = '\0';
if (aBuf)
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the ones here.
return (VarSizeType)strlen(buf); // I seem to remember that the lengths returned from the above API calls aren't consistent in these cases.
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the one here.
return length;
// Formerly the following, but I don't think it's as reliable/future-proof given the 1.0.47 comment above:
//return aBuf
// ? GetCurrentDirectory(MAX_PATH, aBuf)
// : GetCurrentDirectory(0, NULL); // MSDN says that this is a valid way to call it on all OSes, and testing shows that it works on WinXP and 98se.
// Above avoids subtracting 1 to be conservative and to reduce code size (due to the need to otherwise check for zero and avoid subtracting 1 in that case).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the one here.
return length;
// Formerly the following, but I don't think it's as reliable/future-proof given the 1.0.47 comment above:
//char buf_temp[1]; // Just a fake buffer to pass to some API functions in lieu of a NULL, to avoid any chance of misbehavior. Keep the size at 1 so that API functions will always fail to copy to buf.
//// Sizes/lengths/-1/return-values/etc. have been verified correct.
//return aBuf
// ? GetWindowsDirectory(aBuf, MAX_PATH) // MAX_PATH is kept in case it's needed on Win9x for reasons similar to those in GetEnvironmentVarWin9x().
// : GetWindowsDirectory(buf_temp, 0);
// Above avoids subtracting 1 to be conservative and to reduce code size (due to the need to otherwise check for zero and avoid subtracting 1 in that case).
}
VarSizeType BIV_Temp(char *aBuf, char *aVarName)
{
char buf[MAX_PATH];
VarSizeType length = GetTempPath(MAX_PATH, buf);
if (aBuf)
{
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the one here.
if (length)
{
aBuf += length - 1;
if (*aBuf == '\\') // For some reason, it typically yields a trailing backslash, so omit it to improve friendliness/consistency.
char buf_temp[1]; // Just a fake buffer to pass to some API functions in lieu of a NULL, to avoid any chance of misbehavior. Keep the size at 1 so that API functions will always fail to copy to buf.
// Sizes/lengths/-1/return-values/etc. have been verified correct.
return aBuf ? GetEnvVarReliable("comspec", aBuf) // v1.0.46.08: GetEnvVarReliable() fixes %Comspec% on Windows 9x.
: GetEnvironmentVariable("comspec", buf_temp, 0); // Avoids subtracting 1 to be conservative and to reduce code size (due to the need to otherwise check for zero and avoid subtracting 1 in that case).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
return length;
}
VarSizeType BIV_AppData(char *aBuf, char *aVarName) // Called by multiple callers.
{
char buf[MAX_PATH]; // One caller relies on this being explicitly limited to MAX_PATH.
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
return length;
}
VarSizeType BIV_MyDocuments(char *aBuf, char *aVarName) // Called by multiple callers.
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string).
return length;
}
VarSizeType BIV_Caret(char *aBuf, char *aVarName)
{
if (!aBuf)
return MAX_NUMBER_LENGTH; // Conservative, both for performance and in case the value changes between first and second call.
// These static variables are used to keep the X and Y coordinates in sync with each other, as a snapshot
// of where the caret was at one precise instant in time. This is because the X and Y vars are resolved
// separately by the script, and due to split second timing, they might otherwise not be accurate with
// respect to each other. This method also helps performance since it avoids unnecessary calls to
// ATTACH_THREAD_INPUT.
static HWND sForeWinPrev = NULL;
static DWORD sTimestamp = GetTickCount();
static POINT sPoint;
static BOOL sResult;
// I believe only the foreground window can have a caret position due to relationship with focused control.
HWND target_window = GetForegroundWindow(); // Variable must be named target_window for ATTACH_THREAD_INPUT.
if (!target_window) // No window is in the foreground, report blank coordinate.
{
*aBuf = '\0';
return 0;
}
DWORD now_tick = GetTickCount();
if (target_window != sForeWinPrev || now_tick - sTimestamp > 5) // Different window or too much time has passed.
{
// Otherwise:
ATTACH_THREAD_INPUT
sResult = GetCaretPos(&sPoint);
HWND focused_control = GetFocus(); // Also relies on threads being attached.
if (!(g.CoordMode & COORD_MODE_CARET)) // Using the default, which is coordinates relative to window.
ScreenToWindow(sPoint, target_window);
// Now that all failure conditions have been checked, update static variables for the next caller:
sForeWinPrev = target_window;
sTimestamp = now_tick;
}
else // Same window and recent enough, but did prior call fail? If so, provide a blank result like the prior.
{
if (!sResult)
{
*aBuf = '\0';
return 0;
}
}
// Now the above has ensured that sPoint contains valid coordinates that are up-to-date enough to be used.
_itoa(toupper(aVarName[7]) == 'X' ? sPoint.x : sPoint.y, aBuf, 10); // Always output as decimal vs. hex in this case (so that scripts can use "If var in list" with confidence).
if (MyGetCursorInfo) // v1.0.42.02: This method is used to avoid ATTACH_THREAD_INPUT, which interferes with double-clicking if called repeatedly at a high frequency.
, LoadCursor(NULL, IDC_WAIT)}; // If IDC_HAND were added, it would break existing scripts that rely on Unknown being synonymous with Hand. If ever added, IDC_HAND should return NULL on Win95/NT.
// The order in the below array must correspond to the order in the above array:
// GetFullPathName() is done in addition to ConvertFilespecToCorrectCase() for the following reasons:
// 1) It's currrently the only easy way to get the full path of the directory in which a file resides.
// For example, if a script is passed a filename via command line parameter, that file could be
// either an absolute path or a relative path. If relative, of course it's relative to A_WorkingDir.
// The problem is, the script would have to manually detect this, which would probably take several
// extra steps.
// 2) A_LoopFileLongPath is mostly intended for the following cases, and in all of them it seems
// preferable to have the full/absolute path rather than the relative path:
// a) Files dragged onto a .ahk script when the drag-and-drop option has been enabled via the Installer.
// b) Files passed into the script via command line.
// The below also serves to make a copy because changing the original would yield
// unexpected/inconsistent results in a script that retrieves the A_LoopFileFullPath
// but only conditionally retrieves A_LoopFileLongPath.
if (!GetFullPathName(g.mLoopFile->cFileName, MAX_PATH, buf, &unused))
*buf = '\0'; // It might fail if NtfsDisable8dot3NameCreation is turned on in the registry, and possibly for other reasons.
else
// The below is called in case the loop is being used to convert filename specs that were passed
// in from the command line, which thus might not be the proper case (at least in the path
// portion of the filespec), as shown in the file system:
ConvertFilespecToCorrectCase(buf);
}
if (aBuf)
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the one here.
return (VarSizeType)strlen(buf); // Must explicitly calculate the length rather than using the return value from GetFullPathName(), because ConvertFilespecToCorrectCase() expands 8.3 path components.
// Unlike GetLoopFileShortName(), this function returns blank when there is no short path.
// This is done so that there's a way for the script to more easily tell the difference between
// an 8.3 name not being available (due to the being disabled in the registry) and the short
// name simply being the same as the long name. For example, if short name creation is disabled
// in the registry, A_LoopFileShortName would contain the long name instead, as documented.
// But to detect if that short name is really a long name, A_LoopFileShortPath could be checked
// and if it's blank, there is no short name available.
{
char buf[MAX_PATH] = ""; // Set default.
DWORD length = 0; //
if (g.mLoopFile)
// The loop handler already prepended the script's directory in cFileName for us:
if ( !(length = GetShortPathName(g.mLoopFile->cFileName, buf, MAX_PATH)) )
*buf = '\0'; // It might fail if NtfsDisable8dot3NameCreation is turned on in the registry, and possibly for other reasons.
if (aBuf)
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that (even though it's large enough to hold the string). This is true for ReadRegString()'s API call and may be true for other API calls like the one here.
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that due to the zero-the-unused-part behavior of strlcpy/strncpy.
strcpy(aBuf, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for aBuf can crash when aBuf is actually smaller than that due to the zero-the-unused-part behavior of strlcpy/strncpy.
// We're returning the length of the var's contents, not the size.
{
if (g.GuiEvent == GUI_EVENT_DROPFILES)
{
GuiType *pgui;
UINT u, file_count;
// GUI_EVENT_DROPFILES should mean that g.GuiWindowIndex < MAX_GUI_WINDOWS, but the below will double check
// that in case g.GuiEvent can ever be set to that value as a result of receiving a bogus message in the queue.
if (g.GuiWindowIndex >= MAX_GUI_WINDOWS // The current thread was not launched as a result of GUI action or this is a bogus msg.
|| !(pgui = g_gui[g.GuiWindowIndex]) // Gui window no longer exists. Relies on short-circuit boolean.
|| !pgui->mHdrop // No HDROP (probably impossible unless g.GuiEvent was given a bogus value somehow).
|| !(file_count = DragQueryFile(pgui->mHdrop, 0xFFFFFFFF, NULL, 0))) // No files in the drop (not sure if this is possible).
// All of the above rely on short-circuit boolean order.
{
// Make the dropped-files list blank since there is no HDROP to query (or no files in it).
if (aBuf)
*aBuf = '\0';
return 0;
}
// Above has ensured that file_count > 0
if (aBuf)
{
char buf[MAX_PATH], *cp = aBuf;
UINT length;
for (u = 0; u < file_count; ++u)
{
length = DragQueryFile(pgui->mHdrop, u, buf, MAX_PATH); // MAX_PATH is arbitrary since aBuf is already known to be large enough.
strcpy(cp, buf); // v1.0.47: Must be done as a separate copy because passing a size of MAX_PATH for something that isn't actually that large (though clearly large enoug) due to previous size-estimation phase) can crash because the API may read/write data beyond what it actually needs.
cp += length;
if (u < file_count - 1) // i.e omit the LF after the last file to make parsing via "Loop, Parse" easier.
*cp++ = '\n';
// Although the transcription of files on the clipboard into their text filenames is done
// with \r\n (so that they're in the right format to be pasted to other apps as a plain text
// list), it seems best to use a plain linefeed for dropped files since they won't be going
// onto the clipboard nearly as often, and `n is easier to parse. Also, a script array isn't
// used because large file lists would then consume a lot more of memory because arrays
// are permanent once created, and also there would be wasted space due to the part of each
// variable's capacity not used by the filename.
}
// No need for final termination of string because the last item lacks a newline.
return (VarSizeType)(cp - aBuf); // This is the length of what's in the buffer.
}
else
{
VarSizeType total_length = 0;
for (u = 0; u < file_count; ++u)
total_length += DragQueryFile(pgui->mHdrop, u, NULL, 0);
// Above: MSDN: "If the lpszFile buffer address is NULL, the return value is the required size,
// in characters, of the buffer, not including the terminating null character."
return total_length + file_count - 1; // Include space for a linefeed after each filename except the last.
}
// Don't call DragFinish() because this variable might be referred to again before this thread
// is done. DragFinish() is called by MsgSleep() when the current thread finishes.
}
// Otherwise, this event is not GUI_EVENT_DROPFILES, so use standard modes of operation.
// This is here rather than in script.h with the others because it depends on
// hotkey.h and globaldata.h, which can't be easily included in script.h due to
// mutual dependency issues.
{
// If neither hook is active, default this to the same as the regular idle time:
if (!(g_KeybdHook || g_MouseHook))
return BIV_TimeIdle(aBuf, "");
if (!aBuf)
return MAX_NUMBER_LENGTH; // IMPORTANT: Conservative estimate because tick might change between 1st & 2nd calls.
return (VarSizeType)strlen(ITOA64(GetTickCount() - g_TimeLastInputPhysical, aBuf)); // Switching keyboard layouts/languages sometimes sees to throw off the timestamps of the incoming events in the hook.
}
////////////////////////
// BUILT-IN FUNCTIONS //
////////////////////////
// Interface for DynaCall():
#define DC_MICROSOFT 0x0000 // Default
#define DC_BORLAND 0x0001 // Borland compat
#define DC_CALL_CDECL 0x0010 // __cdecl
#define DC_CALL_STD 0x0020 // __stdcall
#define DC_RETVAL_MATH4 0x0100 // Return value in ST
#define DC_RETVAL_MATH8 0x0200 // Return value in ST
// Based on the code by Ton Plooy <tonp@xs4all.nl>.
// Call the specified function with the given parameters. Build a proper stack and take care of correct
// return value processing.
{
aException = 0; // Set default output parameter for caller.
SetLastError(g.LastError); // v1.0.46.07: In case the function about to be called doesn't change last-error, this line serves to retain the script's previous last-error rather than some arbitrary one produced by AutoHotkey's own internal API calls. This line has no measurable impact on performance.
// Declaring all variables early should help minimize stack interference of C code with asm.
DWORD *our_stack;
int param_size;
DWORD stack_dword, our_stack_size = 0; // Both might have to be DWORD for _asm.
BYTE *cp;
DYNARESULT Res = {0}; // This struct is to be returned to caller by value.
DWORD esp_start, esp_end, dwEAX, dwEDX;
int i, esp_delta; // Declare this here rather than later to prevent C code from interfering with esp.
// Reserve enough space on the stack to handle the worst case of our args (which is currently a
// maximum of 8 bytes per arg). This avoids any chance that compiler-generated code will use
// the stack in a way that disrupts our insertion of args onto the stack.
DWORD reserved_stack_size = aParamCount * 8;
_asm
{
mov our_stack, esp // our_stack is the location where we will write our args (bypassing "push").
sub esp, reserved_stack_size // The stack grows downward, so this "allocates" space on the stack.
}
// "Push" args onto the portion of the stack reserved above. Every argument is aligned on a 4-byte boundary.
// We start at the rightmost argument (i.e. reverse order).
for (i = aParamCount - 1; i > -1; --i)
{
DYNAPARM &this_param = aParam[i]; // For performance and convenience.
// Push the arg or its address onto the portion of the stack that was reserved for our use above.
if (this_param.passed_by_address)
{
stack_dword = (DWORD)(size_t)&this_param.value_int; // Any union member would work.
our_stack_size += param_size; // Must be done before our_stack_size is decremented below. Keep track of how many bytes are on our reserved portion of the stack.
cp = (BYTE *)&this_param.value_int + param_size - 4; // Start at the right side of the arg and work leftward.
while (param_size > 0)
{
stack_dword = *(DWORD *)cp; // Get first four bytes
// Helper function for DllCall(). Updates aDynaParam's type and other attributes.
// Caller has ensured that aBuf contains exactly two strings (though the second can be NULL).
{
char buf[32], *type_string;
int i;
// Up to two iterations are done to cover the following cases:
// No second type because there was no SYM_VAR to get it from:
// blank means int
// invalid is err
// (for the below, note that 2nd can't be blank because var name can't be blank, and the first case above would have caught it if 2nd is NULL)
// 1Blank, 2Invalid: blank (but ensure is_unsigned and passed_by_address get reset)
// 1Blank, 2Valid: 2
// 1Valid, 2Invalid: 1 (second iteration would never have run, so no danger of it having erroneously reset is_unsigned/passed_by_address)
// 1Valid, 2Valid: 1 (same comment)
// 1Invalid, 2Invalid: invalid
// 1Invalid, 2Valid: 2
for (i = 0, type_string = aBuf[0]; i < 2 && type_string; type_string = aBuf[++i])
{
if (toupper(*type_string) == 'U') // Unsigned
{
aDynaParam.is_unsigned = true;
++type_string; // Omit the 'U' prefix from further consideration.
}
else
aDynaParam.is_unsigned = false;
strlcpy(buf, type_string, sizeof(buf)); // Make a modifiable copy for easier parsing below.
// v1.0.30.02: The addition of 'P' allows the quotes to be omitted around a pointer type.
// However, the current detection below relies upon the fact that not of the types currently
// contain the letter P anywhere in them, so it would have to be altered if that ever changes.
char *cp = StrChrAny(buf, "*pP"); // Asterisk or the letter P.
if (cp)
{
aDynaParam.passed_by_address = true;
// Remove trailing options so that stricmp() can be used below.
// Allow optional space in front of asterisk (seems okay even for 'P').
if (cp > buf && IS_SPACE_OR_TAB(cp[-1]))
{
cp = omit_trailing_whitespace(buf, cp - 1);
cp[1] = '\0'; // Terminate at the leftmost whitespace to remove all whitespace and the suffix.
}
else
*cp = '\0'; // Terminate at the suffix to remove it.
}
else
aDynaParam.passed_by_address = false;
if (!*buf)
{
// The following also serves to set the default in case this is the first iteration.
// Set default but perform second iteration in case the second type string isn't NULL.
// In other words, if the second type string is explicitly valid rather than blank,
// it should override the following default:
aDynaParam.type = DLL_ARG_INT; // Assume int. This is relied upon at least for having a return type such as a naked "CDecl".
continue; // OK to do this regardless of whether this is the first or second iteration.
}
else if (!stricmp(buf, "Str")) aDynaParam.type = DLL_ARG_STR; // The few most common types are kept up top for performance.
else if (!stricmp(buf, "Int")) aDynaParam.type = DLL_ARG_INT;
else if (!stricmp(buf, "Short")) aDynaParam.type = DLL_ARG_SHORT;
else if (!stricmp(buf, "Char")) aDynaParam.type = DLL_ARG_CHAR;
else if (!stricmp(buf, "Int64")) aDynaParam.type = DLL_ARG_INT64;
else if (!stricmp(buf, "Float")) aDynaParam.type = DLL_ARG_FLOAT;
else if (!stricmp(buf, "Double")) aDynaParam.type = DLL_ARG_DOUBLE;
// Unnecessary: else if (!stricmp(buf, "None")) aDynaParam.type = DLL_ARG_NONE;
// Otherwise, it's blank or an unknown type, leave it set at the default.
else
{
if (i > 0) // Second iteration.
{
// Reset flags to go with any blank value we're falling back to from the first iteration
// (in case our iteration changed the flags based on bogus contents of the second type_string):
aDynaParam.passed_by_address = false;
aDynaParam.is_unsigned = false;
}
else // First iteration, so aDynaParam.type's value will be set by the second.
continue;
}
// Since above didn't "continue", the type is explicitly valid so "return" to ensure that
// the second iteration doesn't run (in case this is the first iteration):
return;
}
}
void BIF_DllCall(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Stores a number or a SYM_STRING result in aResultToken.
// Sets ErrorLevel to the error code appropriate to any problem that occurred.
// Caller has set up aParam to be viewable as a left-to-right array of params rather than a stack.
// It has also ensured that the array has exactly aParamCount items in it.
// Author: Marcus Sonntag (Ultra)
{
// Set default result in case of early return; a blank value:
aResultToken.symbol = SYM_STRING;
aResultToken.marker = "";
HMODULE hmodule_to_free = NULL; // Set default in case of early goto; mostly for maintainability.
// Check that the mandatory first parameter (DLL+Function) is valid.
// (load-time validation has ensured at least one parameter is present).
void *function; // Will hold the address of the function to be called.
switch(aParam[0]->symbol)
{
case SYM_STRING: // By far the most common, so it's listed first for performance. Also for performance, don't even consider the possibility that a quoted literal string like "33" is a function-address.
function = NULL; // Indicate that no function has been specified yet.
break;
// v1.0.46.08: Allow script to specify the address of a function, which might be useful for
// calling functions that the script discovers through unusual means such as C++ member functions.
case SYM_INTEGER:
// The following is commented out due to rarity because it can only occur via expressions that produce
// a SYM_INTEGER (numeric literals aren't SYM_INTEGER). If the address is negative, the same result
// will occur as for any other invalid address: an ErrorLevel of 0xc0000005.
//if (aParam[0]->value_int64 <= 0) // Must be checked before assigning it to "function" so that negatives are still visible (since "function" is unsigned).
//{
// g_ErrorLevel->Assign("-1"); // Stage 1 error: Invalid first param.
// return;
//}
// Otherwise, assume it's a valid address:
function = (void *)aParam[0]->value_int64;
break;
case SYM_FLOAT:
g_ErrorLevel->Assign("-1"); // Stage 1 error: Invalid first param.
return;
default: // SYM_VAR or SYM_OPERAND (SYM_OPERAND is typically a numeric literal, which it seems best to support since it doesn't add any code size or adversely affect performance).
function = (IsPureNumeric(param1, false, false, false))
? (void *)ATOI64(param1)
: NULL; // Not a pure number, so fall back to normal method of considering it to be path+name.
// Due to rarity, the following is commented out (same as the other check above) because it
// doesn't seem worth the code size to check it:
//if (IsPureNumeric(param1, false, false, false))
//{
// __int64 temp64 = ATOI64(param1);
// if (temp64 <= 0)
// {
// g_ErrorLevel->Assign("-1"); // Stage 1 error: Invalid first param.
// return;
// }
// // Otherwise, assume it's a valid address:
// function = (void *)temp64;
//}
//else // Not a pure number, so fall back to normal method of considering it to be path+name.
// function = NULL; // Indicate that no function has been specified yet.
}
// Determine the type of return value.
DYNAPARM return_attrib = {0}; // Will hold the type and other attributes of the function's return value.
int dll_call_mode = DC_CALL_STD; // Set default. Can be overridden to DC_CALL_CDECL and flags can be OR'd into it.
if (aParamCount % 2) // Odd number of parameters indicates the return type has been omitted, so assume BOOL/INT.
return_attrib.type = DLL_ARG_INT;
else
{
// Check validity of this arg's return type:
ExprTokenType &token = *aParam[aParamCount - 1];
if (IS_NUMERIC(token.symbol)) // The return type should be a string, not something purely numeric.
{
g_ErrorLevel->Assign("-2"); // Stage 2 error: Invalid return type or arg type.
return;
}
char *return_type_string[2];
if (token.symbol == SYM_VAR) // SYM_VAR's Type() is always VAR_NORMAL.
{
return_type_string[0] = token.var->Contents();
return_type_string[1] = token.var->mName; // v1.0.33.01: Improve convenience by falling back to the variable's name if the contents are not appropriate.
}
else
{
return_type_string[0] = token.marker;
return_type_string[1] = NULL;
}
if (!strnicmp(return_type_string[0], "CDecl", 5)) // Alternate calling convention.
return_type_string[1] = NULL; // Must be NULL since return_type_string[1] is the variable's name, by definition, so it can't have any spaces in it, and thus no space delimited items after "Cdecl".
g_ErrorLevel->Assign("-2"); // Stage 2 error: Invalid return type or arg type.
return;
default: // Namely:
//case DLL_ARG_INT:
//case DLL_ARG_SHORT:
//case DLL_ARG_CHAR:
//case DLL_ARG_INT64:
if (arg_as_string)
{
// Support for unsigned values that are 32 bits wide or less is done via ATOI64() since
// it should be able to handle both signed and unsigned values. However, unsigned 64-bit
// values probably require ATOU64(), which will prevent something like -1 from being seen
// as the largest unsigned 64-bit int; but more importantly there are some other issues
// with unsigned 64-bit numbers: The script internals use 64-bit signed values everywhere,
// so unsigned values can only be partially supported for incoming parameters, but probably
// not for outgoing parameters (values the function changed) or the return value. Those
// should probably be written back out to the script as negatives so that other parts of
// the script, such as expressions, can see them as signed values. In other words, if the
// script somehow gets a 64-bit unsigned value into a variable, and that value is larger
// that LLONG_MAX (i.e. too large for ATOI64 to handle), ATOU64() will be able to resolve
// it, but any output parameter should be written back out as a negative if it exceeds
// LLONG_MAX (return values can be written out as unsigned since the script can specify
// signed to avoid this, since they don't need the incoming detection for ATOU()).
if (this_dyna_param.is_unsigned && this_dyna_param.type == DLL_ARG_INT64)
this_dyna_param.value_int64 = (__int64)ATOU64(arg_as_string); // Cast should not prevent called function from seeing it as an undamaged unsigned number.
// Values less than or equal to 32-bits wide always get copied into a single 32-bit value
// because they should be right justified within it for insertion onto the call stack.
if (this_dyna_param.type != DLL_ARG_INT64) // Shift the 32-bit value into the high-order DWORD of the 64-bit value for later use by DynaCall().
this_dyna_param.value_int = (int)this_dyna_param.value_int64; // Force a failure if compiler generates code for this that corrupts the union (since the same method is used for the more obscure float vs. double below).
} // switch (this_dyna_param.type)
} // for() each arg.
if (!function) // The function's address hasn't yet been determined.
{
char param1_buf[MAX_PATH*2], *function_name, *dll_name; // Must use MAX_PATH*2 because the function name is INSIDE the Dll file, and thus MAX_PATH can be exceeded.
// Define the standard libraries here. If they reside in %SYSTEMROOT%\system32 it is not
// necessary to specify the full path (it wouldn't make sense anyway).
, GetModuleHandle("comctl32"), GetModuleHandle("gdi32")}; // user32 is listed first for performance.
static int sStdModule_count = sizeof(sStdModule) / sizeof(HMODULE);
// Make a modifiable copy of param1 so that the DLL name and function name can be parsed out easily:
strlcpy(param1_buf, aParam[0]->symbol == SYM_VAR ? aParam[0]->var->Contents() : aParam[0]->marker, sizeof(param1_buf) - 1); // -1 to reserve space for the "A" suffix later below.
if ( !(function_name = strrchr(param1_buf, '\\')) ) // No DLL name specified, so a search among standard defaults will be done.
{
dll_name = NULL;
function_name = param1_buf;
// Since no DLL was specified, search for the specified function among the standard modules.
for (i = 0; i < sStdModule_count; ++i)
if ( sStdModule[i] && (function = (void *)GetProcAddress(sStdModule[i], function_name)) )
break;
if (!function)
{
// Since the absence of the "A" suffix (e.g. MessageBoxA) is so common, try it that way
// but only here with the standard libraries since the risk of ambiguity (calling the wrong
// function) seems unacceptably high in a custom DLL. For example, a custom DLL might have
// function called "AA" but not one called "A".
strcat(function_name, "A"); // 1 byte of memory was already reserved above for the 'A'.
for (i = 0; i < sStdModule_count; ++i)
if ( sStdModule[i] && (function = (void *)GetProcAddress(sStdModule[i], function_name)) )
break;
}
}
else // DLL file name is explicitly present.
{
dll_name = param1_buf;
*function_name = '\0'; // Terminate dll_name to split it off from function_name.
++function_name; // Set it to the character after the last backslash.
// Get module handle. This will work when DLL is already loaded and might improve performance if
// LoadLibrary is a high-overhead call even when the library already being loaded. If
// GetModuleHandle() fails, fall back to LoadLibrary().
HMODULE hmodule;
if ( !(hmodule = GetModuleHandle(dll_name)) )
if ( !(hmodule = hmodule_to_free = LoadLibrary(dll_name)) )
{
g_ErrorLevel->Assign("-3"); // Stage 3 error: DLL couldn't be loaded.
return;
}
if ( !(function = (void *)GetProcAddress(hmodule, function_name)) )
{
// v1.0.34: If it's one of the standard libraries, try the "A" suffix.
for (i = 0; i < sStdModule_count; ++i)
if (hmodule == sStdModule[i]) // Match found.
{
strcat(function_name, "A"); // 1 byte of memory was already reserved above for the 'A'.
function = (void *)GetProcAddress(hmodule, function_name);
break;
}
}
}
if (!function)
{
g_ErrorLevel->Assign("-4"); // Stage 4 error: Function could not be found in the DLL(s).
goto end;
}
}
////////////////////////
// Call the DLL function
////////////////////////
DWORD exception_occurred; // Must not be named "exception_code" to avoid interfering with MSVC macros.
DYNARESULT return_value; // Doing assignment as separate step avoids compiler warning about "goto end" skipping it.
// The above has also set g_ErrorLevel appropriately.
if (*Var::sEmptyString)
{
// v1.0.45.01 Above has detected that a variable of zero capacity was passed to the called function
// and the function wrote to it (assuming sEmptyString wasn't already trashed some other way even
// before the call). So patch up the empty string to stabilize a little; but it's too late to
// salvage this instance of the program because there's no knowing how much static data adjacent to
// sEmptyString has been overwritten and corrupted.
*Var::sEmptyString = '\0';
// Don't bother with freeing hmodule_to_free since a critical error like this calls for minimal cleanup.
// The OS almost certainly frees it upon termination anyway.
// Call ScriptErrror() so that the user knows *which* DllCall is at fault:
g_script.ScriptError("This DllCall requires a prior VarSetCapacity. The program is now unstable and will exit.");
g_script.ExitApp(EXIT_CRITICAL); // Called this way, it will run the OnExit routine, which is debatable because it could cause more good than harm, but might avoid loss of data if the OnExit routine does something important.
}
// It seems best to have the above take precedence over "exception_occurred" below.
if (exception_occurred)
{
// If the called function generated an exception, I think it's impossible for the return value
// to be valid/meaningful since it the function never returned properly. Confirmation of this
// would be good, but in the meantime it seems best to make the return value an empty string as
// an indicator that the call failed (in addition to ErrorLevel).
aResultToken.symbol = SYM_STRING;
aResultToken.marker = "";
// But continue on to write out any output parameters because the called function might have
// had a chance to update them before aborting.
}
else // The call was successful. Interpret and store the return value.
{
// If the return value is passed by address, dereference it here.
if (return_attrib.passed_by_address)
{
return_attrib.passed_by_address = false; // Because the address is about to be dereferenced/resolved.
case DLL_ARG_INT: // If the function has a void return value (formerly DLL_ARG_NONE), the value assigned here is undefined and inconsequential since the script should be designed to ignore it.
aResultToken.symbol = SYM_INTEGER;
if (return_attrib.is_unsigned)
aResultToken.value_int64 = (UINT)return_value.Int; // Preserve unsigned nature upon promotion to signed 64-bit.
else // Signed.
aResultToken.value_int64 = return_value.Int;
break;
case DLL_ARG_SHORT:
aResultToken.symbol = SYM_INTEGER;
if (return_attrib.is_unsigned)
aResultToken.value_int64 = return_value.Int & 0x0000FFFF; // This also forces the value into the unsigned domain of a signed int.
else // Signed.
aResultToken.value_int64 = (SHORT)(WORD)return_value.Int; // These casts properly preserve negatives.
break;
case DLL_ARG_CHAR:
aResultToken.symbol = SYM_INTEGER;
if (return_attrib.is_unsigned)
aResultToken.value_int64 = return_value.Int & 0x000000FF; // This also forces the value into the unsigned domain of a signed int.
else // Signed.
aResultToken.value_int64 = (char)(BYTE)return_value.Int; // These casts properly preserve negatives.
break;
case DLL_ARG_INT64:
// Even for unsigned 64-bit values, it seems best both for simplicity and consistency to write
// them back out to the script as signed values because script internals are not currently
// equipped to handle unsigned 64-bit values. This has been documented.
aResultToken.symbol = SYM_INTEGER;
aResultToken.value_int64 = return_value.Int64;
break;
case DLL_ARG_FLOAT:
aResultToken.symbol = SYM_FLOAT;
aResultToken.value_double = return_value.Float;
break;
case DLL_ARG_DOUBLE:
aResultToken.symbol = SYM_FLOAT; // There is no SYM_DOUBLE since all floats are stored as doubles.
aResultToken.value_double = return_value.Double;
break;
//default: // Should never be reached unless there's a bug.
// aResultToken.symbol = SYM_STRING;
// aResultToken.marker = "";
} // switch(return_attrib.type)
} // Storing the return value when no exception occurred.
// Store any output parameters back into the input variables. This allows a function to change the
// contents of a variable for the following arg types: String and Pointer to <various number types>.
for (arg_count = 0, i = 1; i < aParamCount; ++arg_count, i += 2) // Same loop as used above, so maintain them together.
{
ExprTokenType &this_param = *aParam[i + 1]; // This and the next are resolved for performance and convenience.
if (this_dyna_param.is_unsigned) // Force omission of the high-order word in case it is non-zero from a parameter that was originally and erroneously larger than a short.
output_var.Assign(this_dyna_param.value_int & 0x0000FFFF); // This also forces the value into the unsigned domain of a signed int.
else // Signed.
output_var.Assign((int)(SHORT)(WORD)this_dyna_param.value_int); // These casts properly preserve negatives.
break;
case DLL_ARG_CHAR:
if (this_dyna_param.is_unsigned) // Force omission of the high-order word in case it is non-zero from a parameter that was originally and erroneously larger than a short.
output_var.Assign(this_dyna_param.value_int & 0x000000FF); // This also forces the value into the unsigned domain of a signed int.
else // Signed.
output_var.Assign((int)(char)(BYTE)this_dyna_param.value_int); // These casts properly preserve negatives.
break;
case DLL_ARG_INT64: // Unsigned and signed are both written as signed for the reasons described elsewhere above.
output_var.Assign(this_dyna_param.value_int64);
break;
case DLL_ARG_FLOAT:
output_var.Assign(this_dyna_param.value_float);
break;
case DLL_ARG_DOUBLE:
output_var.Assign(this_dyna_param.value_double);
break;
}
}
end:
if (hmodule_to_free)
FreeLibrary(hmodule_to_free);
}
void BIF_StrLen(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Caller has ensured that SYM_VAR's Type() is VAR_NORMAL and that it's either not an environment
// variable or the caller wants environment varibles treated as having zero length.
// Result is always an integer (caller has set aResultToken.symbol to a default of SYM_INTEGER, so no need
// to set it here).
{
// Loadtime validation has ensured that there's exactly one actual parameter.
// Calling Length() is always valid for SYM_VAR because SYM_VAR's Type() is always VAR_NORMAL.
? aParam[0]->var->Length() + aParam[0]->var->IsBinaryClip() // i.e. Add 1 if it's binary-clipboard, as documented.
: strlen(ExprTokenToString(*aParam[0], aResultToken.buf)); // Allow StrLen(numeric_expr) for flexibility.
}
void BIF_SubStr(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount) // Added in v1.0.46.
{
// Set default return value in case of early return.
aResultToken.symbol = SYM_STRING;
aResultToken.marker = "";
// Get the first arg, which is the string used as the source of the extraction. Call it "haystack" for clarity.
char haystack_buf[MAX_FORMATTED_NUMBER_LENGTH + 1]; // A separate buf because aResultToken.buf is sometimes used to store the result.
char *haystack = ExprTokenToString(*aParam[0], haystack_buf); // Remember that aResultToken.buf is part of a union, though in this case there's no danger of overwriting it since our result will always be of STRING type (not int or float).
int haystack_length = (int)EXPR_TOKEN_LENGTH(aParam[0], haystack);
// Load-time validation has ensured that at least the first two parameters are present:
int starting_offset = (int)ExprTokenToInt64(*aParam[1]) - 1; // The one-based starting position in haystack (if any). Convert it to zero-based.
if (starting_offset > haystack_length)
return; // Yield the empty string (a default set higher above).
if (starting_offset < 0) // Same convention as RegExMatch/Replace(): Treat a StartingPos of 0 (offset -1) as "start at the string's last char". Similarly, treat negatives as starting further to the left of the end of the string.
{
starting_offset += haystack_length;
if (starting_offset < 0)
starting_offset = 0;
}
int remaining_length_available = haystack_length - starting_offset;
int extract_length;
if (aParamCount < 3) // No length specified, so extract all the remaining length.
extract_length = remaining_length_available;
else
{
if ( !(extract_length = (int)ExprTokenToInt64(*aParam[2])) ) // It has asked to extract zero characters.
return; // Yield the empty string (a default set higher above).
if (extract_length < 0)
{
extract_length += remaining_length_available; // Result is the number of characters to be extracted (i.e. after omitting the number of chars specified in extract_length).
if (extract_length < 1) // It has asked to omit all characters.
return; // Yield the empty string (a default set higher above).
}
else // extract_length > 0
if (extract_length > remaining_length_available)
extract_length = remaining_length_available;
}
// Above has set extract_length to the exact number of characters that will actually be extracted.
char *result = haystack + starting_offset; // This is the result except for the possible need to truncate it below.
if (extract_length == remaining_length_available) // All of haystack is desired (starting at starting_offset).
{
aResultToken.marker = result; // No need for any copying or termination, just send back part of haystack.
return; // Caller and Var:Assign() know that overlap is possible, so this seems safe.
}
// Otherwise, at least one character is being omitted from the end of haystack. So need a more complex method.
if (extract_length <= MAX_FORMATTED_NUMBER_LENGTH) // v1.0.46.01: Avoid malloc() for small strings. However, this improves speed by only 10% in a test where random 25-byte strings were extracted from a 700 KB string (probably because VC++'s malloc()/free() are very fast for small allocations).
aResultToken.marker = aResultToken.buf; // Store the address of the result for the caller.
else
{
// Otherwise, validation higher above has ensured: extract_length < remaining_length_available.
// Caller has provided a NULL circuit_token as a means of passing back memory we allocate here.
// So if we change "result" to be non-NULL, the caller will take over responsibility for freeing that memory.
if ( !(aResultToken.circuit_token = (ExprTokenType *)malloc(extract_length + 1)) ) // Out of memory. Due to rarity, don't display an error dialog (there's currently no way for a built-in function to abort the current thread anyway?)
return; // Yield the empty string (a default set higher above).
aResultToken.marker = (char *)aResultToken.circuit_token; // Store the address of the result for the caller.
aResultToken.buf = (char *)(size_t)extract_length; // MANDATORY FOR USERS OF CIRCUIT_TOKEN: "buf" is being overloaded to store the length for our caller.
aResultToken.marker[extract_length] = '\0'; // Must be done separately from the memcpy() because the memcpy() might just be taking a substring (i.e. long before result's terminator).
}
void BIF_InStr(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
// Load-time validation has already ensured that at least two actual parameters are present.
// Above has assigned SCS_INSENSITIVE (0) or SCS_SENSITIVE (1). If it's insensitive, resolve it to
// be Locale-mode if the StringCaseSense mode is either case-sensitive or Locale-insensitive.
if (g.StringCaseSense != SCS_INSENSITIVE && string_case_sense == SCS_INSENSITIVE) // Ordered for short-circuit performance.
string_case_sense = SCS_INSENSITIVE_LOCALE;
char *found_pos;
__int64 offset = 0; // Set default.
if (aParamCount >= 4) // There is a starting position present.
{
offset = ExprTokenToInt64(*aParam[3]) - 1; // i.e. the fourth arg.
if (offset == -1) // Special mode to search from the right side. Other negative values are reserved for possible future use as offsets from the right side.
aResultToken.value_int64 = found_pos ? (found_pos - haystack + 1) : 0; // +1 to convert to 1-based, since 0 indicates "not found".
return;
}
// Otherwise, offset is less than -1 or >= 0.
// Since InStr("", "") yields 1, it seems consistent for InStr("Red", "", 4) to yield
// 4 rather than 0. The below takes this into account:
if (offset < 0 || offset > // ...greater-than the length of haystack calculated below.
(aParam[0]->symbol == SYM_VAR // LengthIgnoreBinaryClip() is used because InStr() doesn't recognize/support binary-clip, so treat it as a normal string (i.e. find first binary zero via strlen()).
// Returns the compiled RegEx, or NULL on failure.
// This function is called by things other than built-in functions so it should be kept general-purpose.
// Upon failure, if aResultToken!=NULL:
// - ErrorLevel is set to a descriptive string other than "0".
// - *aResultToken is set up to contain an empty string.
// Upon success, the following output parameters are set based on the options that were specified:
// aGetPositionsNotSubstrings
// aExtra
// (but it doesn't change ErrorLevel on success, not even if aResultToken!=NULL)
{
// While reading from or writing to the cache, don't allow another thread entry. This is because
// that thread (or this one) might write to the cache while the other one is reading/writing, which
// could cause loss of data integrity (the hook thread can enter here via #IfWin & SetTitleMatchMode RegEx).
// Together, Enter/LeaveCriticalSection reduce performance by only 1.4% in the tightest possible script
// loop that hits the first cache entry every time. So that's the worst case except when there's an actual
// collision, in which case performance suffers more because internally, EnterCriticalSection() does a
// wait/semaphore operation, which is more costly.
// Finally, the code size of all critical-section features together is less than 512 bytes (uncompressed),
// so like performance, that's not a concern either.
EnterCriticalSection(&g_CriticalRegExCache); // Request ownership of the critical section. If another thread already owns it, this thread will block until the other thread finishes.
// SET UP THE CACHE.
// This is a very crude cache for linear search. Of course, hashing would be better in the sense that it
// would allow the cache to get much larger while still being fast (I believe PHP caches up to 4096 items).
// Binary search might not be such a good idea in this case due to the time required to find the right spot
// to insert a new cache item (however, items aren't inserted often, so it might perform quite well until
// the cache contained thousands of RegEx's, which is unlikely to ever happen in most scripts).
struct pcre_cache_entry
{
// For simplicity (and thus performance), the entire RegEx pattern including its options is cached
// is stored in re_raw and that entire string becomes the RegEx's unique identifier for the purpose
// of finding an entry in the cache. Technically, this isn't optimal because some options like Study
// and aGetPositionsNotSubstrings don't alter the nature of the compiled RegEx. However, the CPU time
// required to strip off some options prior to doing a cache search seems likely to offset much of the
// cache's benefit. So for this reason, as well as rarity and code size issues, this policy seems best.
char *re_raw; // The RegEx's literal string pattern such as "abc.*123".
pcre *re_compiled; // The RegEx in compiled form.
pcre_extra *extra; // NULL unless a study() was done (and NULL even then if study() didn't find anything).
// int pcre_options; // Not currently needed in the cache since options are implicitly inside re_compiled.
bool get_positions_not_substrings;
};
#define PCRE_CACHE_SIZE 100 // Going too high would be counterproductive due to the slowness of linear search (and also the memory utilization of so many compiled RegEx's).
int insert_pos; // v1.0.45.03: This is used to avoid updating sLastInsert until an insert actually occurs (it might not occur if a compile error occurs in the regex, or something else stops it early).
// CHECK IF THIS REGEX IS ALREADY IN THE CACHE.
if (sLastFound == -1) // Cache is empty, so insert this RegEx at the first position.
insert_pos = 0; // A section further below will change sLastFound to be 0.
else
{
// Search the cache to see if it contains the caller-specified RegEx in compiled form.
// First check if the last-found item is a match, since often it will be (such as cases
// where a script-loop executes only one RegEx, and also for SetTitleMatchMode RegEx).
if (!strcmp(aRegEx, sCache[sLastFound].re_raw)) // Match found (case sensitive).
goto match_found; // And no need to update sLastFound because it's already set right.
// Since above didn't find a match, search outward in both directions from the last-found match.
// A bidirectional search is done because consecutively-called regex's tend to be adjacent to each other
// in the array, so performance is improved on average (since most of the time when repeating a previously
// executed regex, that regex will already be in the cache -- so optimizing the finding behavior is
// more important than optimizing the never-found-because-not-cached behavior).
bool go_right;
int i, item_to_check, left, right;
int last_populated_item = (sCache[PCRE_CACHE_SIZE-1].re_compiled) // When the array is full...
? PCRE_CACHE_SIZE - 1 // ...all items must be checked except the one already done earlier.
: sLastInsert; // ...else only the items actually populated need to be checked.
for (go_right = true, left = sLastFound, right = sLastFound, i = 0
; i < last_populated_item // This limits it to exactly the number of items remaining to be checked.
; ++i, go_right = !go_right)
{
if (go_right) // Proceed rightward in the array.
{
right = (right == last_populated_item) ? 0 : right + 1; // Increment or wrap around back to the left side.
item_to_check = right;
}
else // Proceed leftward.
{
left = (left == 0) ? last_populated_item : left - 1; // Decrement or wrap around back to the right side.
item_to_check = left;
}
if (!strcmp(aRegEx, sCache[item_to_check].re_raw)) // Match found (case sensitive).
{
sLastFound = item_to_check;
goto match_found;
}
}
// Since above didn't goto, no match was found nor is one possible. So just indicate the insert position
// for where this RegEx will be put into the cache.
// The following formula is for both cache-full and cache-partially-full. When the cache is full,
// it might not be the best possible formula; but it seems pretty good because it takes a round-robin
// approach to overwriting/discarding old cache entries. A discarded entry might have just been
// used -- or even be sLastFound itself -- but on average, this approach seems pretty good because a
// script loop that uses 50 unique RegEx's will quickly stabilize in the cache so that all 50 of them
// stay compiled/cached until the loop ends.
insert_pos = (sLastInsert == PCRE_CACHE_SIZE-1) ? 0 : sLastInsert + 1; // Formula works for both full and partially-full array.
}
// Since the above didn't goto:
// - This RegEx isn't yet in the cache. So compile it and put it in the cache, then return it to caller.
// - Above is responsible for having set insert_pos to the cache position where the new RegEx will be stored.
// The following macro is for maintainability, to enforce the definition of "default" in multiple places.
// PCRE_NEWLINE_CRLF is the default in AutoHotkey rather than PCRE_NEWLINE_LF because *multiline* haystacks
// that scripts will use are expected to come from:
// 50%: FileRead: Uses `r`n by default, for performance)
// 10%: Clipboard: Normally uses `r`n (includes files copied from Explorer, text data, etc.)
// 20%: UrlDownloadToFile: Testing shows that it varies: e.g. microsoft.com uses `r`n, but `n is probably
// more common due to FTP programs automatically translating CRLF to LF when uploading to UNIX servers.
// 20%: Other sources such as GUI edit controls: It's fairly unusual to want to use RegEx on multiline data
// from GUI controls, but in such case `n is much more common than `r`n.
#define SET_DEFAULT_PCRE_OPTIONS \
{\
pcre_options = PCRE_NEWLINE_CRLF;\
aGetPositionsNotSubstrings = false;\
do_study = false;\
}
#define PCRE_NEWLINE_BITS (PCRE_NEWLINE_CRLF | PCRE_NEWLINE_ANY) // Covers all bits that are used for newline options.
// SET DEFAULT OPTIONS:
int pcre_options;
long long do_study;
SET_DEFAULT_PCRE_OPTIONS
// PARSE THE OPTIONS (if any).
char *pat; // When options-parsing is done, pat will point to the start of the pattern itself.
for (pat = aRegEx;; ++pat)
{
switch(*pat)
{
case 'i': pcre_options |= PCRE_CASELESS; break; // Perl-compatible options.
case 'm': pcre_options |= PCRE_MULTILINE; break; //
case 's': pcre_options |= PCRE_DOTALL; break; //
case 'x': pcre_options |= PCRE_EXTENDED; break; //
case 'A': pcre_options |= PCRE_ANCHORED; break; // PCRE-specific options (uppercase used by convention, even internally by PCRE itself).
case 'D': pcre_options |= PCRE_DOLLAR_ENDONLY; break; //
case 'J': pcre_options |= PCRE_DUPNAMES; break; //
case 'U': pcre_options |= PCRE_UNGREEDY; break; //
case 'X': pcre_options |= PCRE_EXTRA; break; //
case '\a':pcre_options = (pcre_options & ~PCRE_NEWLINE_BITS) | PCRE_NEWLINE_ANY; break; // v1.0.46.06: alert/bell (i.e. `a) is used for PCRE_NEWLINE_ANY.
case '\n':pcre_options = (pcre_options & ~PCRE_NEWLINE_BITS) | PCRE_NEWLINE_LF; break; // See below.
// Above option: Could alternatively have called it "LF" rather than or in addition to "`n", but that
// seems slightly less desirable due to potential overlap/conflict with future option letters,
// plus the fact that `n should be pretty well known to AutoHotkey users, especially advanced ones
// using RegEx. Note: `n`r is NOT treated the same as `r`n because there's a slight chance PCRE
// will someday support `n`r for some obscure usage (or just for symmetry/completeness).
// The PCRE_NEWLINE_XXX options are valid for both compile() and exec(), but specifying it for exec()
// would only serve to override the default stored inside the compiled pattern (seems rarely needed).
case '\r':
if (pat[1] == '\n') // Even though `r`n is the default, it's recognized as an option for flexibility and intuitiveness.
{
++pat; // Skip over the second character so that it's not recognized as a separate option by the next iteration.
pcre_options = (pcre_options & ~PCRE_NEWLINE_BITS) | PCRE_NEWLINE_CRLF; // Set explicitly in case it was unset by an earlier option. Remember that PCRE_NEWLINE_CRLF is a bitwise combination of PCRE_NEWLINE_LF and CR.
}
else // For completeness, it's easy to support PCRE_NEWLINE_CR too, though nowadays I think it's quite rare (former Macintosh format).
pcre_options = (pcre_options & ~PCRE_NEWLINE_BITS) | PCRE_NEWLINE_CR; // Do it this way because PCRE_NEWLINE_CRLF is a bitwise combination of PCRE_NEWLINE_CR and PCRE_NEWLINE_LF.
break;
// Other options (uppercase so that lowercase can be reserved for future/PERL options):
case 'P': aGetPositionsNotSubstrings = true; break;
case 'S': do_study = true; break;
case ' ': // Allow only spaces and tabs as fillers so that everything else is protected/reserved for
case '\t': // future use (such as future PERL options).
break;
case ')': // This character, when not escaped, marks the normal end of the options section. We know it's not escaped because if it had been, the loop would have stopped at the backslash before getting here.
++pat; // Set pat to be the start of the actual RegEx pattern, and leave options set to how they were by any prior iterations above.
goto break_both;
default: // Namely the following:
//case '\0': No options are present, so ignore any letters that were accidentally recognized and treat entire string as the pattern.
//case '(' : An open parenthesis must be considered an invalid option because otherwise it would be ambiguous with a subpattern.
//case '\\': In addition to backslash being an invalid option, it also covers "\)" as being invalid (i.e. so that it's never necessary to check for an escaped close-parenthesis).
//case all-other-chars: All others are invalid options; so like backslash above, ignore any letters that were accidentally recognized and treat entire string as the pattern.
SET_DEFAULT_PCRE_OPTIONS // Revert to original options in case any early letters happened to match valid options.
pat = aRegEx; // Indicate that the entire string is the pattern (no options).
// To distinguish between a bad option and no options at all (for better error reporting), could check if
// within the next few chars there's an unmatched close-parenthesis (non-escaped). If so, the user
// intended some options but one of them was invalid. However, that would be somewhat difficult to do
// because both \) and [)] are valid regex patterns that contain an unmatched close-parenthesis.
// Since I don't know for sure if there are other cases (or whether future RegEx extensions might
// introduce more such cases), it seems best not to attempt to distinguish. Using more than two options
// is extremely rare anyway, so syntax errors of this type do not happen often (and the only harm done
// is a misleading error message from PCRE rather than something like "Bad option"). In addition,
// omitting it simplifies the code and slightly improves performance.
goto break_both;
} // switch(*pat)
} // for()
break_both:
// Reaching here means that pat has been set to the beginning of the RegEx pattern itself and all options
// "this_entry.pcre_options" doesn't exist because it isn't currently needed in the cache. This is
// because the RE's options are implicitly stored inside re_compiled.
sLastInsert = insert_pos; // v1.0.45.03: Must be done only *after* the insert succeeded because some things rely on sLastInsert being synonymous with the last populated item in the cache (when the cache isn't yet full).
sLastFound = sLastInsert; // Relied upon in the case where sLastFound==-1. But it also sets things up to start the search at this item next time, because it's a bit more likely to be found here such as tight loops containing only one RegEx.
// Remember that although sLastFound==sLastInsert in this case, it isn't always so -- namely when a previous
// call found an existing match in the cache without having to compile and insert the item.
LeaveCriticalSection(&g_CriticalRegExCache);
return re_compiled; // Indicate success.
match_found: // RegEx was found in the cache at position sLastFound, so return the cached info back to the caller.
if (captured_pattern_count < 0) // PCRE_ERROR_NOMATCH or some kind of error.
return NULL;
// Otherwise, captured_pattern_count>=0 (it's 0 when offset[] was too small; but that's harmless in this case).
return aHaystack + offset[0]; // Return the position of the entire-pattern match.
}
void RegExReplace(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount
, pcre *aRE, pcre_extra *aExtra, char *aHaystack, int aHaystackLength, int aStartingOffset
, int aOffset[], int aNumberOfIntsInOffset)
{
// Set default return value in case of early return.
aResultToken.symbol = SYM_STRING;
aResultToken.marker = aHaystack; // v1.0.46.06: aHaystack vs. "" is the new default because it seems a much safer and more convenient to return aHaystack when an unexpected PCRE-exec error occurs (such an error might otherwise cause loss of data in scripts that don't meticulously check ErrorLevel after each RegExReplace()).
// If an output variable was provided for the count, resolve it early in case of early goto.
// Fix for v1.0.47.05: In the unlikely event that output_var_count is the same script-variable as
// as the haystack, needle, or replacement (i.e. the same memory), don't set output_var_count until
// immediately prior to returning. Otherwise, haystack, needle, or replacement would corrupted while
// it's still being used here.
Var *output_var_count = (aParamCount > 3 && aParam[3]->symbol == SYM_VAR) ? aParam[3]->var : NULL; // SYM_VAR's Type() is always VAR_NORMAL.
int replacement_count = 0; // This value will be stored in output_var_count, but only at the very end due to the reason above.
// Get the replacement text (if any) from the incoming parameters. If it was omitted, treat it as "".
, *substring_name_pos, substring_name[33] // In PCRE, "Names consist of up to 32 alphanumeric characters and underscores."
, transform;
// Caller has provided a NULL circuit_token as a means of passing back memory we allocate here.
// So if we change "result" to be non-NULL, the caller will take over responsibility for freeing that memory.
char *&result = (char *&)aResultToken.circuit_token; // Make an alias to type-cast and for convenience.
int &result_length = (int &)aResultToken.buf; // MANDATORY FOR USERS OF CIRCUIT_TOKEN: "buf" is being overloaded to store the length for our caller.
result_size = 0; // And caller has already set "result" to be NULL. The buffer is allocated only upon
result_length = 0; // first use to avoid a potentially massive allocation that might be wasted and cause swapping (not to mention that we'll have better ability to estimate the correct total size after the first replacement is discovered).
// Below uses a temp variable because realloc() returns NULL on failure but leaves original block allocated.
// Note that if it's given a NULL pointer, realloc() does a malloc() instead.
char *realloc_temp;
#define REGEX_REALLOC(size) \
{\
result_size = size;\
if ( !(realloc_temp = (char *)realloc(result, result_size)) )\
goto out_of_mem;\
result = realloc_temp;\
}
// See if a replacement limit was specified. If not, use the default (-1 means "replace all").
int limit = (aParamCount > 4) ? (int)ExprTokenToInt64(*aParam[4]) : -1;
// aStartingOffset is altered further on in the loop; but for its initial value, the caller has ensured
// that it lies within aHaystackLength. Also, if there are no replacements yet, haystack_pos ignores
// aStartingOffset because otherwise, when the first replacement occurs, any part of haystack that lies
// to the left of a caller-specified aStartingOffset wouldn't get copied into the result.
for (empty_string_is_not_a_match = 0, haystack_pos = aHaystack
;; haystack_pos = aHaystack + aStartingOffset) // See comment above.
{
// Execute the expression to find the next match.
captured_pattern_count = (limit == 0) ? PCRE_ERROR_NOMATCH // Only when limit is exactly 0 are we done replacing. All negative values are "replace all".
// This situation happens when a previous iteration found a match but it was the empty string.
// That iteration told the pcre_exec that just occurred above to try to match something other than ""
// at the same position. But since we're here, it wasn't able to find such a match. So just copy
// the current character over literally then advance to the next character to resume normal searching.
empty_string_is_not_a_match = 0; // Reset so that the next iteration starts off with the normal matching method.
result[result_length++] = *haystack_pos; // This can't overflow because the size calculations in a previous iteration reserved 3 bytes: 1 for this character, 1 for the possible LF that follows CR, and 1 for the terminator.
++aStartingOffset; // Advance to next candidate section of haystack.
// v1.0.46.06: This following section was added to avoid finding a match between a CR and LF
// when PCRE_NEWLINE_ANY mode is in effect. The fact that this is the only change for
// PCRE_NEWLINE_ANY relies on the belief that any pattern that matches the empty string in between
// a CR and LF must also match the empty string that occurs right before the CRLF (even if that
// pattern also matched a non-empty string right before the empty one in front of the CRLF). If
// this belief is correct, no logic similar to this is needed near the bottom of the main loop
// because the empty string found immediately prior to this CRLF will put us into
// empty_string_is_not_a_match mode, which will then execute this section of code (unless
// empty_string_is_not_a_match mode actually found a match, in which case the logic here seems
// superseded by that match?) Even if this reasoning is not a complete solution, it might be
// adequate if patterns that match empty strings are rare, which I believe they are. In fact,
// they might be so rare that arguably this could be documented as a known limitation rather than
// having added the following section of code in the first place.
if (*haystack_pos == '\r' && haystack_pos[1] == '\n')
{
// pcre_fullinfo() is a fast call, so it's called every time to simplify the code (I don't think
// this whole "empty_string_is_not_a_match" section of code executes for most patterns anyway,
// so performance seems less of a concern).
if (!pcre_fullinfo(aRE, aExtra, PCRE_INFO_OPTIONS, &pcre_options) // Success.
&& (pcre_options & PCRE_NEWLINE_ANY)) // See comment above.
{
result[result_length++] = '\n'; // This can't overflow because the size calculations in a previous iteration reserved 3 bytes: 1 for this character, 1 for the possible LF that follows CR, and 1 for the terminator.
++aStartingOffset; // Skip over this LF because it "belongs to" the CR that preceded it.
}
}
continue; // i.e. we're not done yet because the "no match" above was a special one and there's still more haystack to check.
}
// Otherwise, there aren't any more matches. So we're all done except for copying the last part of
// haystack into the result (if applicable).
if (replacement_count) // And by definition, result!=NULL due in this case to prior iterations.
{
if (haystack_portion_length = aHaystackLength - aStartingOffset) // This is the remaining part of haystack that needs to be copied over as-is.
REGEX_REALLOC(new_result_length + 1); // This will end the loop if an alloc error occurs.
memcpy(result + result_length, haystack_pos, haystack_portion_length); // memcpy() usually benches a little faster than strcpy().
result_length = new_result_length; // Remember that result_length is actually an output for our caller, so even if for no other reason, it must be kept accurate for that.
}
result[result_length] = '\0'; // result!=NULL when replacement_count!=0. Also, must terminate it unconditionally because other sections usually don't do it.
// Set RegExMatch()'s return value to be "result":
aResultToken.marker = result; // Caller will take care of freeing result's memory.
}
// Section below is obsolete but is retained for its comments.
//else // No replacements were actually done, so just return the original string to avoid malloc+memcpy
// (in addition, returning the original might help the caller make other optimizations).
//{
// Already set as a default earlier, so commented out:
//aResultToken.marker = aHaystack;
// Not necessary to set output-var (length) for caller except when we allocated memory for the caller:
//result_length = aHaystackLength; // result_length is an alias for an output parameter, so update for maintainability even though currently callers don't use it when no alloc of circuit_token.
//
// There's no need to do the following because it should already be that way when replacement_count==0.
//if (result)
// free(result);
//result = NULL; // This tells the caller that we already freed it (i.e. from its POV, we never allocated anything).
//}
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // All done, indicate success via ErrorLevel.
goto set_count_and_return; //
}
// Otherwise:
if (captured_pattern_count < 0) // An error other than "no match". These seem very rare, so it seems best to abort rather than yielding a partially-converted result.
{
g_ErrorLevel->Assign(captured_pattern_count); // No error text is stored; just a negative integer (since these errors are pretty rare).
goto set_count_and_return; // Goto vs. break to leave aResultToken.marker set to aHaystack and replacement_count set to 0, and let ErrorLevel tell the story.
}
// Otherwise (since above didn't return, break, or continue), a match has been found (i.e. captured_pattern_count > 0;
// it should never be 0 in this case because that only happens when offset[] is too small, which it isn't).
++replacement_count;
--limit; // It's okay if it goes below -1 because all negatives are treated as "replace all".
match_pos = aHaystack + aOffset[0]; // This is the location in aHaystack of the entire-pattern match.
haystack_portion_length = (int)(match_pos - haystack_pos); // The length of the haystack section between the end of the previous match and the start of the current one.
// Handle this replacement by making two passes through the replacement-text: The first calculates the size
// (which avoids having to constantly check for buffer overflow with potential realloc at multiple stages).
// The second iteration copies the replacement (along with any literal text in haystack before it) into the
// result buffer (which was expanded if necessary by the first iteration).
for (second_iteration = 0; second_iteration < 2; ++second_iteration) // second_iteration is used as a boolean for readability.
{
if (second_iteration)
{
// Using the required length calculated by the first iteration, expand/realloc "result" if necessary.
if (new_result_length + 3 > result_size) // Must use +3 not +1 in case of empty_string_is_not_a_match (which needs room for up to two extra characters).
{
// The first expression passed to REGEX_REALLOC is the average length of each replacement so far.
// It's more typically more accurate to pass that than the following "length of current
// Above is the length difference between the current replacement text and what it's
// replacing (it's negative when replacement is smaller than what it replaces).
REGEX_REALLOC(PredictReplacementSize((new_result_length - aOffset[1]) / replacement_count // See above.
, replacement_count, limit, aHaystackLength, new_result_length+2, aOffset[1])); // +2 in case of empty_string_is_not_a_match (which needs room for up to two extra characters). The function will also do another +1 to convert length to size (for terminator).
// The above will return if an alloc error occurs.
}
//else result_size is not only large enough, but also non-zero. Other sections rely on it always
// being non-zero when replacement_count>0.
// Before doing the actual replacement and its backreferences, copy over the part of haystack that
dest = result + result_length; // Init dest for use by the loops further below.
}
else // i.e. it's the first iteration, so begin calculating the size required.
new_result_length = result_length + haystack_portion_length; // Init length to the part of haystack before the match (it must be copied over as literal text).
// DOLLAR SIGN ($) is the only method supported because it simplifies the code, improves performance,
// and avoids the need to escape anything other than $ (which simplifies the syntax).
for (src = replacement; ; ++src) // For each '$' (increment to skip over the symbol just found by the inner for()).
{
// Find the next '$', if any.
src_orig = src; // Init once for both loops below.
if (second_iteration) // Mode: copy src-to-dest.
{
while (*src && *src != '$') // While looking for the next '$', copy over everything up until the '$'.
*dest++ = *src++;
result_length += (int)(src - src_orig);
}
else // This is the first iteration (mode: size-calculation).
{
for (; *src && *src != '$'; ++src); // Find the next '$', if any.
new_result_length += (int)(src - src_orig); // '$' or '\0' was found: same expansion either way.
}
if (!*src) // Reached the end of the replacement text.
break; // Nothing left to do, so if this is the first major iteration, begin the second.
// Otherwise, a '$' has been found. Check if it's a backreference and handle it.
// But first process any special flags that are present.
transform = '\0'; // Set default. Indicate "no transformation".
extra_offset = 0; // Set default. Indicate that there's no need to hop over an extra character.
if (char_after_dollar = src[1]) // This check avoids calling toupper on '\0', which directly or indirectly causes an assertion error in CRT.
char_after_dollar = src[2]; // Ignore the transform character for the purposes of backreference recognition further below.
break;
//else leave things at their defaults.
}
}
//else leave things at their defaults.
ref_num = INT_MIN; // Set default to "no valid backreference". Use INT_MIN to virtually guaranty that anything other than INT_MIN means that something like a backreference was found (even if it's invalid, such as ${-5}).
switch (char_after_dollar)
{
case '{': // Found a backreference: ${...
substring_name_pos = src + 2 + extra_offset;
if (closing_brace = strchr(substring_name_pos, '}'))
{
if (substring_name_length = (int)(closing_brace - substring_name_pos))
{
if (substring_name_length < sizeof(substring_name))
{
strlcpy(substring_name, substring_name_pos, substring_name_length + 1); // +1 to convert length to size, which truncates the new string at the desired position.
if (IsPureNumeric(substring_name, true, false, true)) // Seems best to allow floating point such as 1.0 because it will then get truncated to an integer. It seems to rare that anyone would want to use floats as names.
ref_num = atoi(substring_name); // Uses atoi() vs. ATOI to avoid potential overlap with non-numeric names such as ${0x5}, which should probably be considered a name not a number? In other words, seems best not to make some names that start with numbers "special" just because they happen to be hex numbers.
else // For simplicity, no checking is done to ensure it consiss of the "32 alphanumeric characters and underscores". Let pcre_get_stringnumber() figure that out for us.
ref_num = pcre_get_stringnumber(aRE, substring_name); // Returns a negative on failure, which when stored in ref_num is relied upon as an inticator.
}
//else it's too long, so it seems best (debatable) to treat it as a unmatched/unfound name, i.e. "".
src = closing_brace; // Set things up for the next iteration to resume at the char after "${..}"
}
//else it's ${}, so do nothing, which in effect will treat it all as literal text.
}
//else unclosed '{': for simplicity, do nothing, which in effect will treat it all as literal text.
break;
case '$': // i.e. Two consecutive $ amounts to one literal $.
++src; // Skip over the first '$', and the loop's increment will skip over the second. "extra_offset" is ignored due to rarity and silliness. Just transcribe things like $U$ as U$ to indicate the problem.
break; // This also sets up things properly to copy a single literal '$' into the result.
case '\0': // i.e. a single $ was found at the end of the string.
break; // Seems best to treat it as literal (strictly speaking the script should have escaped it).
default:
if (char_after_dollar >= '0' && char_after_dollar <= '9') // Treat it as a single-digit backreference. CONSEQUENTLY, $15 is really $1 followed by a literal '5'.
{
ref_num = char_after_dollar - '0'; // $0 is the whole pattern rather than a subpattern.
src += 1 + extra_offset; // Set things up for the next iteration to resume at the char after $d. Consequently, $19 is seen as $1 followed by a literal 9.
}
//else not a digit: do nothing, which treats a $x as literal text (seems ok since like $19, $name will never be supported due to ambiguity; only ${name}).
} // switch (char_after_dollar)
if (ref_num == INT_MIN) // Nothing that looks like backreference is present (or the very unlikely ${-2147483648}).
{
if (second_iteration)
{
*dest++ = *src; // src is incremented by the loop. Copy only one character because the enclosing loop will take care of copying the rest.
++result_length; // Update the actual length.
}
else
++new_result_length; // Update the calculated length.
// And now the enclosing loop will take care of the characters beyond src.
}
else // Something that looks like a backreference was found, even if it's invalid (e.g. ${-5}).
{
// It seems to improve convenience and flexibility to transcribe a nonexistent backreference
// as a "" rather than literally (e.g. putting a ${1} literally into the new string). Although
// putting it in literally has the advantage of helping debugging, it doesn't seem to outweigh
// the convenience of being able to specify nonexistent subpatterns. MORE IMPORANTLY a subpattern
// might not exist per se if it hasn't been matched, such as an "or" like (abc)|(xyz), at least
// when it's the last subpattern, in which case it should definitely be treated as "" and not
// copied over literally. So that would have to be checked for if this is changed.
if (ref_num >= 0 && ref_num < captured_pattern_count) // Treat ref_num==0 as reference to the entire-pattern's match.
{
if (match_length = aOffset[ref_num*2 + 1] - aOffset[ref_num*2])
dest[match_length] = '\0'; // Terminate for use below (shouldn't cause overflow because REALLOC reserved space for terminator; nor should there be any need to undo the termination afterward).
switch(transform)
{
case 'U': CharUpper(dest); break;
case 'L': CharLower(dest); break;
case 'T': StrToTitleCase(dest); break;
}
}
dest += match_length;
result_length += match_length;
}
else // First iteration.
new_result_length += match_length;
}
}
//else subpattern doesn't exist (or its invalid such as ${-5}, so treat it as blank because:
// 1) It's boosts script flexibility and convenience (at the cost of making it hard to detect
// script bugs, which would be assisted by transcribing ${999} as literal text rather than "").
// 2) It simplifies the code.
// 3) A subpattern might not exist per se if it hasn't been matched, such as "(abc)|(xyz)"
// (in which case only one of them is matched). If such a thing occurs at the end
// of the RegEx pattern, captured_pattern_count might not include it. But it seems
// pretty clear that it should be treated as "" rather than some kind of error condition.
}
} // for() (for each '$')
} // for() (a 2-iteration for-loop)
// If we're here, a match was found.
// Technique and comments from pcredemo.c:
// If the previous match was NOT for an empty string, we can just start the next match at the end
// of the previous one.
// If the previous match WAS for an empty string, we can't do that, as it would lead to an
// infinite loop. Instead, a special call of pcre_exec() is made with the PCRE_NOTEMPTY and
// PCRE_ANCHORED flags set. The first of these tells PCRE that an empty string is not a valid match;
// other possibilities must be tried. The second flag restricts PCRE to one match attempt at the
// initial string position. If this match succeeds, an alternative to the empty string match has been
// found, and we can proceed round the loop.
//
// The following may be one example of this concept:
// In the string "xy", replace the pattern "x?" by "z". The traditional/proper answer (achieved by
// the logic here) is "zzyz" because: 1) The first x is replaced by z; 2) The empty string before y
// is replaced by z; 3) the logic here applies PCRE_NOTEMPTY to search again at the same position, but
// that search doesn't find a match; so the logic higher above advances to the next character (y) and
// continues the search; it finds the empty string at the end of haystack, which is replaced by z.
// On the other hand, maybe there's a better example than the above that explains what would happen
// if PCRE_NOTEMPTY actually finds a match, or what would happen if this PCRE_NOTEMPTY method weren't
// used at all (i.e. infinite loop as mentioned in the previous paragraph).
//
// If this match is "" (length 0), then by definitition we just found a match in normal mode, not
// PCRE_NOTEMPTY mode (since that mode isn't capable of finding ""). Thus, empty_string_is_not_a_match
// is currently 0.
if (aOffset[0] == aOffset[1]) // A match was found but it's "".
empty_string_is_not_a_match = PCRE_NOTEMPTY | PCRE_ANCHORED; // Try to find a non-"" match at the same position by switching to an alternate mode and doing another iteration.
//else not an empty match, so advance to next candidate section of haystack and resume searching.
aStartingOffset = aOffset[1]; // In either case, set starting offset to the candidate for the next search.
} // for()
// All paths above should return (or goto some other label), so execution should never reach here except
// through goto:
out_of_mem:
// Due to extreme rarity and since this is a regex execution error of sorts, use PCRE's own error code.
g_ErrorLevel->Assign(PCRE_ERROR_NOMEMORY);
if (result)
{
free(result); // Since result is probably an non-terminated string (not to mention an incompletely created result), it seems best to free it here to remove it from any further consideration by the caller.
result = NULL; // Tell caller that it was freed.
// AND LEAVE aResultToken.marker (i.e. the final result) set to aHaystack, because the altered result is
// indeterminate and thus discarded.
}
// Now fall through to below so that count is set even for out-of-memory error.
set_count_and_return:
if (output_var_count)
output_var_count->Assign(replacement_count); // v1.0.47.05: Must be done last in case output_var_count shares the same memory with haystack, needle, or replacement.
}
void BIF_RegEx(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// This function is the initial entry point for both RegExMatch() and RegExReplace().
// Caller has set aResultToken.symbol to a default of SYM_INTEGER.
{
bool mode_is_replace = toupper(aResultToken.marker[5]) == 'R'; // Union's marker initially contains the function name; e.g. RegEx[R]eplace.
char *needle = ExprTokenToString(*aParam[1], aResultToken.buf); // Load-time validation has already ensured that at least two actual parameters are present.
return; // It already set ErrorLevel and aResultToken for us. If caller provided an output var/array, it is not changed under these conditions because there's no way of knowing how many subpatterns are in the RegEx, and thus no way of knowing how far to init the array.
// Since compiling succeeded, get info about other parameters.
char *haystack = ExprTokenToString(*aParam[0], haystack_buf); // Load-time validation has already ensured that at least two actual parameters are present.
int haystack_length = (int)EXPR_TOKEN_LENGTH(aParam[0], haystack);
int param_index = mode_is_replace ? 5 : 3;
int starting_offset;
if (aParamCount <= param_index)
starting_offset = 0; // The one-based starting position in haystack (if any). Convert it to zero-based.
if (starting_offset < 0) // Same convention as SubStr(): Treat a StartingPos of 0 (offset -1) as "start at the string's last char". Similarly, treat negatives as starting further to the left of the end of the string.
{
starting_offset += haystack_length;
if (starting_offset < 0)
starting_offset = 0;
}
else if (starting_offset > haystack_length)
// Although pcre_exec() seems to work properly even without this check, its absence would allow
// the empty string to be found beyond the length of haystack, which could lead to problems and is
// probably more trouble than its worth (assuming it has any worth -- perhaps for a pattern that
// looks backward from itself; but that seems too rare to support and might create code that's
// harder to maintain, especially in RegExReplace()).
starting_offset = haystack_length; // Due to rarity of this condition, opt for simplicity: just point it to the terminator, which is in essence an empty string (which will cause result in "no match" except when searcing for "").
}
// SET UP THE OFFSET ARRAY, which consists of int-pairs containing the start/end offset of each match.
int pattern_count;
pcre_fullinfo(re, extra, PCRE_INFO_CAPTURECOUNT, &pattern_count); // The number of capturing subpatterns (i.e. all except (?:xxx) I think). Failure is not checked because it seems too unlikely in this case.
++pattern_count; // Increment to include room for the entire-pattern match.
int number_of_ints_in_offset = pattern_count * 3; // PCRE uses 3 ints for each (sub)pattern: 2 for offsets and 1 for its internal use.
int *offset = (int *)_alloca(number_of_ints_in_offset * sizeof(int)); // _alloca() boosts performance and seems safe because subpattern_count would usually have to be ridiculously high to cause a stack overflow.
if (mode_is_replace) // Handle RegExReplace() completely then return.
{
RegExReplace(aResultToken, aParam, aParamCount
, re, extra, haystack, haystack_length, starting_offset, offset, number_of_ints_in_offset);
return;
}
// OTHERWISE, THIS IS RegExMatch() not RegExReplace().
// SET THE RETURN VALUE AND ERRORLEVEL BASED ON THE RESULTS OF EXECUTING THE EXPRESSION.
if (captured_pattern_count == PCRE_ERROR_NOMATCH)
{
g_ErrorLevel->Assign(ERRORLEVEL_NONE); // i.e. "no match" isn't an error.
aResultToken.value_int64 = 0;
// BUT CONTINUE ON so that the output-array (if any) is fully reset (made blank), which improves
// convenience for the script.
}
else if (captured_pattern_count < 0) // An error other than "no match".
{
g_ErrorLevel->Assign(captured_pattern_count); // No error text is stored; just a negative integer (since these errors are pretty rare).
aResultToken.symbol = SYM_STRING;
aResultToken.marker = "";
}
else // Match found, and captured_pattern_count <= 0 (but should never be 0 in this case because that only happens when offset[] is too small, which it isn't).
{
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
aResultToken.value_int64 = offset[0] + 1; // i.e. the position of the entire-pattern match is the function's return value.
}
if (aParamCount < 3 || aParam[2]->symbol != SYM_VAR) // No output var, so nothing more to do.
return;
// OTHERWISE, THE CALLER PROVIDED AN OUTPUT VAR/ARRAY: Store the substrings that matched the patterns.
Var &output_var = *aParam[2]->var; // SYM_VAR's Type() is always VAR_NORMAL.
char *mem_to_free = NULL; // Set default.
if (get_positions_not_substrings) // In this mode, it's done this way to avoid creating an array if there are no subpatterns; i.e. the return value is the starting position and the array name will contain the length of what was found.
output_var.Assign(captured_pattern_count < 0 ? 0 : offset[1] - offset[0]); // Seems better to store length of zero rather than something non-length like -1 (after all, the return value is blank in this case, which should be used as the error indicator).
else
{
if (captured_pattern_count < 0) // Failed or no match.
output_var.Assign(); // Make the full-pattern substring blank as a further indicator, and for convenience consistency in the script.
else // Greater than 0 (it can't be equal to zero because offset[] was definitely large enough).
{
// Fix for v1.0.45.07: The following check allow haystack to be the same script-variable as the
// output-var/array. Unless a copy of haystack is made, any subpatterns to be populated after the
// entire-pattern output-var below would be corrupted. In other words, anything that refers to the
// contents of haystack after the output-var has been assigned would otherwise refer to the wrong
// string. Note that the following isn't done for the get_positions_not_substrings mode higher above
// because that mode never refers to haystack when populating its subpatterns.
if (pattern_count > 1 && haystack == output_var.Contents()) // i.e. there are subpatterns to be output afterward, and haystack is the same variable as the output-var that's about to be overwritten below.
if (mem_to_free = _strdup(haystack)) // _strdup() is very tiny and basically just calls strlen+malloc+strcpy.
haystack = mem_to_free;
//else due to the extreme rarity of running out of memory AND SIMULTANEOUSLY having output-var match
// haystack, continue on so that at least partial success is achieved (the only thing that will
// be wrong in this case is the subpatterns, if any).
output_var.Assign(haystack + offset[0], offset[1] - offset[0]); // It shouldn't be possible for the full-pattern match's offset to be -1, since if where here, a match on the full pattern was always found.
}
}
if (pattern_count < 2) // There are no subpatterns (only the main pattern), so nothing more to do.
goto free_and_return;
// OTHERWISE, CONTINUE ON TO STORE THE SUBSTRINGS THAT MATCHED THE SUBPATTERNS (EVEN IF PCRE_ERROR_NOMATCH).
// For lookup performance, create a table of subpattern names indexed by subpattern number.
char **subpat_name = NULL; // Set default as "no subpattern names present or available".
bool allow_dupe_subpat_names = false; // Set default.
char *name_table;
int name_count, name_entry_size;
if ( !pcre_fullinfo(re, extra, PCRE_INFO_NAMECOUNT, &name_count) // Success. Fix for v1.0.45.01: Don't check captured_pattern_count>=0 because PCRE_ERROR_NOMATCH can still have named patterns!
&& name_count // There's at least one named subpattern. Relies on short-circuit boolean order.
subpat_name = (char **)_alloca(subpat_array_size); // See other use of _alloca() above for reasons why it's used.
ZeroMemory(subpat_name, subpat_array_size); // Set default for each index to be "no name corresponds to this subpattern number".
for (int i = 0; i < name_count; ++i, name_table += name_entry_size)
{
// Below converts first two bytes of each name-table entry into the pattern number (it might be
// possible to simplify this, but I'm not sure if big vs. little-endian will ever be a concern).
subpat_name[(name_table[0] << 8) + name_table[1]] = name_table + 2; // For indexing simplicity, subpat_name[0] is for the main/entire pattern though it is never actually used for that because it can't be named without being enclosed in parentheses (in which case it becomes a subpattern).
// For simplicity and unlike PHP, IsPureNumeric() isn't called to forbid numeric subpattern names.
// It seems the worst than could happen if it is numeric is that it would overlap/overwrite some of
// the numerically-indexed elements in the output-array. Seems pretty harmless given the rarity.
}
}
//else one of the pcre_fullinfo() calls may have failed. The PCRE docs indicate that this realistically never
// happens unless bad inputs were given. So due to rarity, just leave subpat_name==NULL; i.e. "no named subpatterns".
// Make var_name longer than Max so that FindOrAddVar() will be able to spot and report var names
// that are too long, either because the base-name is too long, or the name becomes too long
// as a result of appending the array index number:
char var_name[MAX_VAR_NAME_LENGTH + 68]; // Allow +3 extra for "Len" and "Pos" suffixes, +1 for terminator, and +64 for largest sub-pattern name (actually it's 32, but 64 allows room for future expansion). 64 is also enough room for the largest 64-bit integer, 20 chars: 18446744073709551616
strcpy(var_name, output_var.mName); // This prefix is copied in only once, for performance.
char *var_name_suffix = var_name + prefix_length; // The position at which to copy the sequence number (index).
int always_use = output_var.IsLocal() ? ALWAYS_USE_LOCAL : ALWAYS_USE_GLOBAL;
int n, p = 1, *this_offset = offset + 2; // Init for both loops below.
Var *array_item;
bool subpat_not_matched;
if (get_positions_not_substrings)
{
int subpat_pos, subpat_len;
for (; p < pattern_count; ++p, this_offset += 2) // Start at 1 because above already did pattern #0 (the full pattern).
{
subpat_not_matched = (p >= captured_pattern_count || this_offset[0] < 0); // See comments in similar section below about this.
if (subpat_not_matched)
{
subpat_pos = 0;
subpat_len = 0;
}
else // NOTE: The formulas below work even for a capturing subpattern that wasn't actually matched, such as one of the following: (abc)|(123)
{
subpat_pos = this_offset[0] + 1; // One-based (i.e. position zero means "not found").
subpat_len = this_offset[1] - this_offset[0]; // It seemed more convenient for scripts to store Length instead of an ending offset.
}
if (subpat_name && subpat_name[p]) // This subpattern number has a name, so store it under that name.
{
if (*subpat_name[p]) // This check supports allow_dupe_subpat_names. See comments below.
{
suffix_length = sprintf(var_name_suffix, "Pos%s", subpat_name[p]); // Append the subpattern to the array's base name.
if (array_item = g_script.FindOrAddVar(var_name, prefix_length + suffix_length, always_use))
array_item->Assign(subpat_pos);
suffix_length = sprintf(var_name_suffix, "Len%s", subpat_name[p]); // Append the subpattern name to the array's base name.
if (array_item = g_script.FindOrAddVar(var_name, prefix_length + suffix_length, always_use))
array_item->Assign(subpat_len);
// Fix for v1.0.45.01: Section below added. See similar section further below for comments.
if (!subpat_not_matched && allow_dupe_subpat_names) // Explicitly check subpat_not_matched not pos/len so that behavior is consistent with the default mode (non-position).
for (n = p + 1; n < pattern_count; ++n) // Search to the right of this subpat to find others with the same name.
if (subpat_name[n] && !stricmp(subpat_name[n], subpat_name[p])) // Case-insensitive because unlike PCRE, named subpatterns conform to AHK convention of insensitive variable names.
subpat_name[n] = ""; // Empty string signals subsequent iterations to skip it entirely.
}
//else an empty subpat name caused by "allow duplicate names". Do nothing (see comments above).
}
else // This subpattern has no name, so write it out as its pattern number instead. For performance and memory utilization, it seems best to store only one or the other (named or number), not both.
{
// For comments about this section, see the similar for-loop later below.
suffix_length = sprintf(var_name_suffix, "Pos%d", p); // Append the element number to the array's base name.
if (array_item = g_script.FindOrAddVar(var_name, prefix_length + suffix_length, always_use))
array_item->Assign(subpat_pos);
//else var couldn't be created: no error reporting currently, since it basically should never happen.
suffix_length = sprintf(var_name_suffix, "Len%d", p); // Append the element number to the array's base name.
if (array_item = g_script.FindOrAddVar(var_name, prefix_length + suffix_length, always_use))
array_item->Assign(subpat_len);
}
}
goto free_and_return;
} // if (get_positions_not_substrings)
// Otherwise, we're in get-substring mode (not offset mode), so store the substring that matches each subpattern.
for (; p < pattern_count; ++p, this_offset += 2) // Start at 1 because above already did pattern #0 (the full pattern).
{
// If both items in this_offset are -1, that means the substring wasn't populated because it's
// subpattern wasn't needed to find a match (or there was no match for *anything*). For example:
// "(xyz)|(abc)" (in which only one is subpattern will match).
// NOTE: PCRE isn't clear on this, but it seems likely that captured_pattern_count
// (returned from pcre_exec()) can be less than pattern_count (from pcre_fullinfo/
// PCRE_INFO_CAPTURECOUNT). So the below takes this into account by not trusting values
// in offset[] that are beyond captured_pattern_count. Further evidence of this is PCRE's
// pcre_copy_substring() function, which consults captured_pattern_count to decide whether to
// consult the offset array. The formula below works even if captured_pattern_count==PCRE_ERROR_NOMATCH.
if (subpat_name && subpat_name[p]) // This subpattern number has a name, so store it under that name.
{
if (*subpat_name[p]) // This check supports allow_dupe_subpat_names. See comments below.
{
// This section is similar to the one in the "else" below, so see it for more comments.
strcpy(var_name_suffix, subpat_name[p]); // Append the subpat name to the array's base name. strcpy() seems safe because PCRE almost certainly enforces the 32-char limit on subpattern names.
if (array_item = g_script.FindOrAddVar(var_name, 0, always_use))
{
if (subpat_not_matched)
array_item->Assign(); // Omit all parameters to make the var empty without freeing its memory (for performance, in case this RegEx is being used many times in a loop).
else
{
if (p < pattern_count-1 // i.e. there's at least one more subpattern after this one (if there weren't, making a copy of haystack wouldn't be necessary because overlap can't harm this final assignment).
&& haystack == array_item->Contents()) // For more comments, see similar section higher above.
// Fix for v1.0.45.01: When the J option (allow duplicate named subpatterns) is in effect,
// PCRE returns entries for all the duplicates. But we don't want an unmatched duplicate
// to overwrite a previously matched duplicate. To prevent this, when we're here (i.e.
// this subpattern matched something), mark duplicate entries in the names array that lie
// to the right of this item to indicate that they should be skipped by subsequent iterations.
if (allow_dupe_subpat_names)
for (n = p + 1; n < pattern_count; ++n) // Search to the right of this subpat to find others with the same name.
if (subpat_name[n] && !stricmp(subpat_name[n], subpat_name[p])) // Case-insensitive because unlike PCRE, named subpatterns conform to AHK convention of insensitive variable names.
subpat_name[n] = ""; // Empty string signals subsequent iterations to skip it entirely.
}
}
//else var couldn't be created: no error reporting currently, since it basically should never happen.
}
//else an empty subpat name caused by "allow duplicate names". Do nothing (see comments above).
}
else // This subpattern has no name, so instead write it out as its actual pattern number. For performance and memory utilization, it seems best to store only one or the other (named or number), not both.
{
_itoa(p, var_name_suffix, 10); // Append the element number to the array's base name.
// To help performance (in case the linked list of variables is huge), tell it where
// to start the search. Use the base array name rather than the preceding element because,
// for example, Array19 is alphabetially less than Array2, so we can't rely on the
// numerical ordering:
if (array_item = g_script.FindOrAddVar(var_name, 0, always_use))
{
if (subpat_not_matched)
array_item->Assign(); // Omit all parameters to make the var empty without freeing its memory (for performance, in case this RegEx is being used many times in a loop).
else
{
if (p < pattern_count-1 // i.e. there's at least one more subpattern after this one (if there weren't, making a copy of haystack wouldn't be necessary because overlap can't harm this final assignment).
&& haystack == array_item->Contents()) // For more comments, see similar section higher above.
void BIF_Chr(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
int param1 = (int)ExprTokenToInt64(*aParam[0]); // Convert to INT vs. UINT so that negatives can be detected.
char *cp = aResultToken.buf; // If necessary, it will be moved to a persistent memory location by our caller.
if (param1 < 0 || param1 > 255)
*cp = '\0'; // Empty string indicates both Chr(0) and an out-of-bounds param1.
else
{
cp[0] = param1;
cp[1] = '\0';
}
aResultToken.symbol = SYM_STRING;
aResultToken.marker = cp;
}
void BIF_NumGet(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
size_t right_side_bound, target; // Don't make target a pointer-type because the integer offset might not be a multiple of 4 (i.e. the below increments "target" directly by "offset" and we don't want that to use pointer math).
ExprTokenType &target_token = *aParam[0];
if (target_token.symbol == SYM_VAR) // SYM_VAR's Type() is always VAR_NORMAL.
{
target = (size_t)target_token.var->Contents();
right_side_bound = target + target_token.var->Capacity(); // This is first illegal address to the right of target.
}
else
target = (size_t)ExprTokenToInt64(target_token);
if (aParamCount > 1) // Parameter "offset" is present, so increment the address by that amount. For flexibility, this is done even when the target isn't a variable.
target += (int)ExprTokenToInt64(*aParam[1]); // Cast to int vs. size_t to support negative offsets.
BOOL is_signed;
size_t size = 4; // Set default.
if (aParamCount < 3) // The "type" parameter is absent (which is most often the case), so use defaults.
++type; // Remove the first character from further consideration.
is_signed = FALSE;
}
else
is_signed = TRUE;
switch(toupper(*type)) // Override "size" and aResultToken.symbol if type warrants it. Note that the above has omitted the leading "U", if present, leaving type as "Int" vs. "Uint", etc.
{
case 'I':
if (strchr(type, '6')) // Int64. It's checked this way for performance, and to avoid access violation if string is bogus and too short such as "i64".
size = 8;
//else keep "size" at its default set earlier.
break;
case 'S': size = 2; break; // Short.
case 'C': size = 1; break; // Char.
case 'D': size = 8; // Double. NO "BREAK": fall through to below.
case 'F': // OR FELL THROUGH FROM ABOVE.
aResultToken.symbol = SYM_FLOAT; // Override the default of SYM_INTEGER set by our caller.
// In the case of 'F', leave "size" at its default set earlier.
break;
// default: For any unrecognized values, keep "size" and aResultToken.symbol at their defaults set earlier
// (for simplicity).
}
}
// If the target is a variable, the following check ensures that the memory to be read lies within its capacity.
// This seems superior to an exception handler because exception handlers only catch illegal addresses,
// not ones that are technically legal but unintentionally bugs due to being beyond a variable's capacity.
// Moreover, __try/except is larger in code size. Another possible alternative is IsBadReadPtr()/IsBadWritePtr(),
// but those are discouraged by MSDN.
// The following aren't covered by the check below:
// - Due to rarity of negative offsets, only the right-side boundary is checked, not the left.
// - Due to rarity and to simplify things, Float/Double (which "return" higher above) aren't checked.
if (target < 1024 // Basic sanity check to catch incoming raw addresses that are zero or blank.
|| target_token.symbol == SYM_VAR && target+size > right_side_bound) // i.e. it's ok if target+size==right_side_bound because the last byte to be read is actually at target+size-1. In other words, the position of the last possible terminator within the variable's capacity is considered an allowable address.
{
aResultToken.symbol = SYM_STRING;
aResultToken.marker = "";
return;
}
switch(size)
{
case 4: // Listed first for performance.
if (aResultToken.symbol == SYM_FLOAT)
aResultToken.value_double = *(float *)target;
else if (is_signed)
aResultToken.value_int64 = *(int *)target; // aResultToken.symbol was set to SYM_FLOAT or SYM_INTEGER higher above.
else
aResultToken.value_int64 = *(unsigned int *)target;
break;
case 8:
// The below correctly copies both DOUBLE and INT64 into the union.
// Unsigned 64-bit integers aren't supported because variables/expressions can't support them.
aResultToken.value_int64 = *(__int64 *)target;
break;
case 2:
if (is_signed) // Don't use ternary because that messes up type-casting.
aResultToken.value_int64 = *(short *)target;
else
aResultToken.value_int64 = *(unsigned short *)target;
break;
default: // size 1
if (is_signed) // Don't use ternary because that messes up type-casting.
void BIF_NumPut(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
// Load-time validation has ensured that at least the first two parameters are present.
ExprTokenType &token_to_write = *aParam[0];
size_t right_side_bound, target; // Don't make target a pointer-type because the integer offset might not be a multiple of 4 (i.e. the below increments "target" directly by "offset" and we don't want that to use pointer math).
ExprTokenType &target_token = *aParam[1];
if (target_token.symbol == SYM_VAR) // SYM_VAR's Type() is always VAR_NORMAL.
{
target = (size_t)target_token.var->Contents();
right_side_bound = target + target_token.var->Capacity(); // This is first illegal address to the right of target.
}
else
target = (size_t)ExprTokenToInt64(target_token);
if (aParamCount > 2) // Parameter "offset" is present, so increment the address by that amount. For flexibility, this is done even when the target isn't a variable.
target += (int)ExprTokenToInt64(*aParam[2]); // Cast to int vs. size_t to support negative offsets.
size_t size = 4; // Set defaults.
BOOL is_integer = TRUE; //
if (aParamCount > 3) // The "type" parameter is present (which is somewhat unusual).
if (toupper(*type) == 'U') // Unsigned; but in the case of NumPut, it doesn't matter so ignore it.
++type; // Remove the first character from further consideration.
switch(toupper(*type)) // Override "size" and is_integer if type warrants it. Note that the above has omitted the leading "U", if present, leaving type as "Int" vs. "Uint", etc.
{
case 'I':
if (strchr(type, '6')) // Int64. It's checked this way for performance, and to avoid access violation if string is bogus and too short such as "i64".
size = 8;
//else keep "size" at its default set earlier.
break;
case 'S': size = 2; break; // Short.
case 'C': size = 1; break; // Char.
case 'D': size = 8; // Double. NO "BREAK": fall through to below.
case 'F': // OR FELL THROUGH FROM ABOVE.
is_integer = FALSE; // Override the default set earlier.
// In the case of 'F', leave "size" at its default set earlier.
break;
// default: For any unrecognized values, keep "size" and is_integer at their defaults set earlier
// (for simplicity).
}
}
aResultToken.value_int64 = target + size; // This is used below and also as NumPut's return value. It's the address to the right of the item to be written. aResultToken.symbol was set to SYM_INTEGER by our caller.
// See comments in NumGet about the following section:
if (target < 1024 // Basic sanity check to catch incoming raw addresses that are zero or blank.
|| target_token.symbol == SYM_VAR && aResultToken.value_int64 > right_side_bound) // i.e. it's ok if target+size==right_side_bound because the last byte to be read is actually at target+size-1. In other words, the position of the last possible terminator within the variable's capacity is considered an allowable address.
case 'T': key_state_type = KEYSTATE_TOGGLE; break; // Whether a toggleable key such as CapsLock is currently turned on.
case 'P': key_state_type = KEYSTATE_PHYSICAL; break; // Physical state of key.
default: key_state_type = KEYSTATE_LOGICAL;
}
// Caller has set aResultToken.symbol to a default of SYM_INTEGER, so no need to set it here.
aResultToken.value_int64 = ScriptGetKeyState(vk, key_state_type); // 1 for down and 0 for up.
}
void BIF_VarSetCapacity(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Returns: The variable's new capacity.
// Parameters:
// 1: Target variable (unquoted).
// 2: Requested capacity.
// 3: Byte-value to fill the variable with (e.g. 0 to have the same effect as ZeroMemory).
{
// Caller has set aResultToken.symbol to a default of SYM_INTEGER, so no need to set it here.
aResultToken.value_int64 = 0; // Set default. In spite of being ambiguous with the result of Free(), 0 seems a little better than -1 since it indicates "no capacity" and is also equal to "false" for easy use in expressions.
if (aParam[0]->symbol == SYM_VAR) // SYM_VAR's Type() is always VAR_NORMAL.
{
Var &var = *aParam[0]->var; // For performance and convenience. Note: SYM_VAR's Type() is always VAR_NORMAL.
if (aParamCount > 1) // Second parameter is present.
var.Close(); // v1.0.44.14: Removes the VAR_ATTRIB_BINARY_CLIP (if it was present) because it seems more flexible to convert binary-to-normal rather than checking IsBinaryClip() then doing nothing if it binary.
return;
}
// Since above didn't return:
if (new_capacity)
{
var.Assign(NULL, new_capacity, true, false); // This also destroys the variables contents.
VarSizeType capacity;
if (aParamCount > 2 && (capacity = var.Capacity()) > 1) // Third parameter is present and var has enough capacity to make FillMemory() meaningful.
{
--capacity; // Convert to script-POV capacity. To avoid underflow, do this only now that Capacity() is known not to be zero.
// The following uses capacity-1 because the last byte of a variable should always
// be left as a binary zero to avoid crashes and problems due to unterminated strings.
// In other words, a variable's usable capacity from the script's POV is always one
// less than its actual capacity:
BYTE fill_byte = (BYTE)ExprTokenToInt64(*aParam[2]); // For simplicity, only numeric characters are supported, not something like "a" to mean the character 'a'.
char *contents = var.Contents();
FillMemory(contents, capacity, fill_byte); // Last byte of variable is always left as a binary zero.
contents[capacity] = '\0'; // Must terminate because nothing else is explicitly reponsible for doing it.
var.Length() = fill_byte ? capacity : 0; // Length is same as capacity unless fill_byte is zero.
}
else
// By design, Assign() has already set the length of the variable to reflect new_capacity.
// This is not what is wanted in this case since it should be truly empty.
var.Length() = 0;
}
else // ALLOC_SIMPLE, due to its nature, will not actually be freed, which is documented.
var.Free();
} // if (aParamCount > 1)
//else the var is not altered; instead, the current capacity is reported, which seems more intuitive/useful than having it do a Free().
if (aResultToken.value_int64 = var.Capacity()) // Don't subtract 1 here in lieu doing it below (avoids underflow).
--aResultToken.value_int64; // Omit the room for the zero terminator since script capacity is defined as length vs. size.
} // (aParam[0]->symbol == SYM_VAR)
}
void BIF_FileExist(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
char filename_buf[MAX_FORMATTED_NUMBER_LENGTH + 1]; // Because aResultToken.buf is used for something else below.
if ( !(func = g_script.FindFunc(func_name)) ) // Nonexistent function.
return; // Yield the default return value set earlier.
// If too many formal parameters or any are ByRef/optional, indicate failure.
// This helps catch bugs in scripts that are assigning the wrong function to a monitor.
// It also preserves additional parameters for possible future use (i.e. avoids breaking
// existing scripts if more formal parameters are supported in a future version).
if (func->mIsBuiltIn || func->mParamCount > 4 || func->mMinParams < func->mParamCount) // Too many params, or some are optional.
return; // Yield the default return value set earlier.
for (int i = 0; i < func->mParamCount; ++i) // Check if any formal parameters are ByRef.
if (func->mParam[i].is_byref)
return; // Yield the default return value set earlier.
}
else // Explicitly blank function name ("") means delete this item. By contrast, an omitted second parameter means "give me current function of this message".
mode_is_delete = true;
}
// If this is the first use of the g_MsgMonitor array, create it now rather than later to reduce code size
// and help the maintainability of sections further below. The following relies on short-circuit boolean order:
if (!g_MsgMonitor && !(g_MsgMonitor = (MsgMonitorStruct *)malloc(sizeof(MsgMonitorStruct) * MAX_MSG_MONITORS)))
return; // Yield the default return value set earlier.
// Check if this message already exists in the array:
int msg_index;
for (msg_index = 0; msg_index < g_MsgMonitorCount; ++msg_index)
if (aParamCount < 2) // Single-parameter mode: Report existing item's function name.
return; // Everything was already set up above to yield the proper return value.
// Otherwise, an existing item is being assigned a new function (the function has already
// been verified valid above). Continue on to update this item's attributes.
}
else // This message doesn't exist in array yet.
{
if (mode_is_delete || aParamCount < 2) // Delete or report function-name of a non-existent item.
return; // Yield the default return value set earlier (an empty string).
// Since above didn't return, the message is to be added as a new element. The above already
// verified that func is not NULL.
if (msg_index == MAX_MSG_MONITORS) // No room in array.
return; // Indicate failure by yielding the default return value set earlier.
// Otherwise, the message is to be added, so increment the total:
++g_MsgMonitorCount;
strcpy(buf, func->mName); // Yield the NEW name as an indicator of success. Caller has ensured that buf large enough to support max function name.
aResultToken.marker = buf;
monitor.instance_count = 0; // Reset instance_count only for new items since existing items might currently be running.
// Continue on to the update-or-create logic below.
}
// Since above didn't return, above has ensured that msg_index is the index of the existing or new
// MsgMonitorStruct in the array. In addition, it has set the proper return value for us.
// Update those struct attributes that get the same treatment regardless of whether this is an update or creation.
monitor.msg = specified_msg;
monitor.func = func;
if (aParamCount > 2)
monitor.max_instances = (short)ExprTokenToInt64(*aParam[2]); // No validation because it seems harmless if it's negative or some huge number.
else // Unspecified, so if this item is being newly created fall back to the default.
if (!item_already_exists)
monitor.max_instances = 1;
}
struct RCCallbackFunc // Used by BIF_RegisterCallback() and related.
{
ULONG data1; //E8 00 00 00
ULONG data2; //00 8D 44 24
ULONG data3; //08 50 FF 15
UINT (__stdcall **callfuncptr)(UINT*,char*);
ULONG data4; //59 84 C4 nn
USHORT data5; //FF E1
//code ends
UCHAR actual_param_count; // This is the actual (not formal) number of parameters passed from the caller to the callback. Kept adjacent to the USHORT above to conserve memory due to 4-byte struct alignment.
bool create_new_thread; // Kept adjacent to above to conserve memory due to 4-byte struct alignment.
DWORD event_info; // A_EventInfo
Func *func; // The UDF to be called whenever the callback's caller calls callfuncptr.
};
UINT __stdcall RegisterCallbackCStub(UINT *params, char *address) // Used by BIF_RegisterCallback().
// JGR: On Win32 parameters are always 4 bytes wide. The exceptions are functions which work on the FPU stack
// (not many of those). Win32 is quite picky about the stack always being 4 byte-aligned, (I have seen only one
// application which defied that and it was a patched ported DOS mixed mode application). The Win32 calling
// convention assumes that the parameter size equals the pointer size. 64 integers on Win32 are passed on
// pointers, or as two 32 bit halves for some functionsà
{
#define DEFAULT_CB_RETURN_VALUE 0 // The value returned to the callback's caller if script doesn't provide one.
RCCallbackFunc &cb = *((RCCallbackFunc*)(address-5)); //second instruction is 5 bytes after start (return address pushed by call)
Func &func = *cb.func; // For performance and convenience.
// NOTES ABOUT INTERRUPTIONS / CRITICAL:
// An incoming call to a callback is considered an "emergency" for the purpose of determining whether
// critical/high-priority threads should be interrupted because there's no way easy way to buffer or
// postpone the call. Therefore, NO check of the following is done here:
// - Current thread's priority (that's something of a deprecated feature anyway).
// - Current thread's status of Critical (however, Critical would prevent us from ever being called in
// cases where the callback is triggered indirectly via message/dispatch due to message filtering
// and/or Critical's ability to pump messes less often).
// - INTERRUPTIBLE_IN_EMERGENCY (which includes g_MenuIsVisible and g_AllowInterruption), which primarily
// affects SLEEP_WITHOUT_INTERRUPTION): It's debatable, but to maximize flexibility it seems best to allow
// callbacks during the display of a menu and during SLEEP_WITHOUT_INTERRUPTION. For most callers of
// SLEEP_WITHOUT_INTERRUPTION, interruptions seem harmless. For some it could be a problem, but when you
// consider how rare such callbacks are (mostly just subclassing of windows/controls) and what those
// callbacks tend to do, conflicts seem very rare.
// Of course, a callback can also be triggered through explicit script action such as a DllCall of
// EnumWindows, in which case the script would want to be interrupted unconditionally to make the call.
// However, in those cases it's hard to imagine that INTERRUPTIBLE_IN_EMERGENCY wouldn't be true anyway.
if (cb.create_new_thread && g_nThreads >= g_MaxThreadsTotal) // Since this is a callback, it seems too rare to make an exemption for functions whose first command is ExitApp.
return DEFAULT_CB_RETURN_VALUE;
// Need to check if backup of function's variables is needed in case:
// 1) The UDF is assigned to more than one callback, in which case the UDF could be running more than one
// simultantously.
// 2) The callback is intended to be reentrant (e.g. a subclass/WindowProc that doesn't Critical).
// 3) Script explicitly calls the UDF in addition to using it as a callback.
//
// See ExpandExpression() for detailed comments about the following section.
VarBkp *var_backup = NULL; // If needed, it will hold an array of VarBkp objects.
int var_backup_count; // The number of items in the above array.
if (func.mInstances > 0) // Backup is needed.
if (!Var::BackupFunctionVars(func, var_backup, var_backup_count)) // Out of memory.
return DEFAULT_CB_RETURN_VALUE; // Since out-of-memory is so rare, it seems justifiable not to have any error reporting and instead just avoid calling the function.
global_struct global_saved;
char ErrorLevel_saved[ERRORLEVEL_SAVED_SIZE];
DWORD EventInfo_saved;
if (cb.create_new_thread)
{
// See MsgSleep() for comments about the following section.
else // Backup/restore only A_EventInfo. This avoids callbacks changing A_EventInfo for the current thread/context (that would be counterintuitive and a source of script bugs).
EventInfo_saved = g.EventInfo;
g.EventInfo = cb.event_info; // This is the means to identify which caller called the callback (if the script assigned more than one caller to this callback).
g_script.UpdateTrayIcon(); // Doesn't measurably impact performance (unless icon needs to be changed, which it generally won't in the case of fast-mode because by definition the current thread isn't paused). This is necessary because it's possible the tray icon shows "paused" if this callback was called via message (e.g. when subclassing a control).
// The following section is similar to the one in ExpandExpression(). See it for detailed comments.
int i;
for (i = 0; i < cb.actual_param_count; ++i) // For each formal parameter that has a matching actual (an earlier stage already verified that there are enough formals to cover the actuals).
func.mParam[i].var->Assign((DWORD)params[i]); // All parameters are passed "by value" because an earlier stage ensured there are no ByRef parameters.
for (; i < func.mParamCount; ++i) // For each remaining formal (i.e. those that lack actuals), apply a default value (an earlier stage verified that all such parameters have a default-value available).
{
FuncParam &this_formal_param = func.mParam[i]; // For performance and convenience.
// The following isn't necessary because an earlier stage has already ensured that there
case PARAM_DEFAULT_STR: this_formal_param.var->Assign(this_formal_param.default_str); break;
case PARAM_DEFAULT_INT: this_formal_param.var->Assign(this_formal_param.default_int64); break;
case PARAM_DEFAULT_FLOAT: this_formal_param.var->Assign(this_formal_param.default_double); break;
//case PARAM_DEFAULT_NONE: Not possible due to validation at an earlier stage.
}
}
g_script.mLastScriptRest = g_script.mLastPeekTime = GetTickCount(); // Somewhat debatable, but might help minimize interruptions when the callback is called via message (e.g. subclassing a control; overriding a WindowProc).
char *return_value;
func.Call(return_value); // Call the UDF.
// MUST handle return_value BEFORE calling FreeAndRestoreFunctionVars() because return_value might be
// the contents of one of the function's local variables (which are about to be free'd).
UINT number_to_return = *return_value ? ATOU(return_value) : DEFAULT_CB_RETURN_VALUE; // No need to check the following because they're implied for *return_value!=0: result != EARLY_EXIT && result != FAIL;
if ( actual_param_count > func->mParamCount // The function doesn't have enough formals to cover the specified number of actuals.
|| actual_param_count < func->mMinParams ) // ...or the function has too many mandatory formals (caller specified insufficient actuals to cover them all).
return; // Indicate failure by yielding the default result set earlier.
}
else // Default to the number of mandatory formal parameters in the function's definition.
actual_param_count = func->mMinParams;
if (actual_param_count > 31) // The ASM instruction currently used limits parameters to 31 (which should be plenty for any realistic use).
return; // Indicate failure by yielding the default result set earlier.
// To improve callback performance, ensure there are no ByRef parameters (for simplicity: not even ones that
// have default values). This avoids the need to ensure formal parameters are non-aliases each time the
// callback is called.
for (int i = 0; i < func->mParamCount; ++i)
if (func->mParam[i].is_byref)
return; // Yield the default return value set earlier.
// GlobalAlloc() and dynamically-built code is the means by which a script can have an unlimited number of
// distinct callbacks. On Win32, GlobalAlloc is the same function as LocalAlloc: they both point to
// RtlAllocateHeap on the process heap. For large chunks of code you would reserve a 64K section with
// VirtualAlloc and fill that, but for the 32 bytes we use here that would be overkill; GlobalAlloc is
// much more efficient. MSDN says about GlobalAlloc: "All memory is created with execute access; no
// special function is required to execute dynamically generated code. Memory allocated with this function
// is guaranteed to be aligned on an 8-byte boundary."
RCCallbackFunc *callbackfunc=(RCCallbackFunc*) GlobalAlloc(GMEM_FIXED,sizeof(RCCallbackFunc)); //allocate structure off process heap, automatically RWE and fixed.
if(!callbackfunc) return;
RCCallbackFunc &cb = *callbackfunc; // For convenience and possible code-size reduction.
cb.data1=0xE8; // call +0 -- E8 00 00 00 00 ;get eip, stays on stack as parameter 2 for C function (char *address).
cb.data3=0x15FF5008; // push eax -- 50 ;eax pushed on stack as parameter 1 for C stub (UINT *params)
// call [xxxx] (in the lines below) -- FF 15 xx xx xx xx ;call C stub __stdcall, so stack cleaned up for us.
// Comments about the static variable below: The reason for using the address of a pointer to a function,
// is that the address is passed as a fixed address, whereas a direct call is passed as a 32-bit offset
// relative to the beginning of the next instruction, which is more fiddly than it's worth to calculate
// for dynamic code, as a relative call is designed to allow position independent calls to within the
// same memory block without requiring dynamic fixups, or other such inconveniences. In essence:
// call xxx ; is relative
// call [ptr_xxx] ; is position independent
// Typically the latter is used when calling imported functions, etc., as only the pointers (import table),
// need to be adjusted, not the calls themselves...
static UINT (__stdcall *funcaddrptr)(UINT*,char*)=RegisterCallbackCStub; // Use fixed absolute address of pointer to function, instead of varying relative offset to function.
cb.callfuncptr=&funcaddrptr; // xxxx: Address of C stub.
cb.data4=0xC48359 // pop ecx -- 59 ;return address... add esp, xx -- 83 C4 xx ;stack correct (add argument to add esp, nn for stack correction).
+ (StrChrAny(options, "Cc") ? 0 : actual_param_count<<26); // Recognize "C" as the "CDecl" option.
edge += (int)ExprTokenToInt64(*aParam[new_part_count]); // For code simplicity, no check for negative (seems fairly harmless since the bar will simply show up with the wrong number of parts to indicate the problem).
part[new_part_count] = edge;
}
// For code simplicity, there is currently no means to have the last part of the bar use less than
// all of the bar's remaining width. The desire to do so seems rare, especially since the script can
// add an extra/unused part at the end to achieve nearly (or perhaps exactly) the same effect.
part[new_part_count++] = -1; // Make the last part use the remaining width of the bar.
old_part_count = SendMessage(control_hwnd, SB_GETPARTS, 0, NULL); // MSDN: "This message always returns the number of parts in the status bar [regardless of how it is called]".
if (old_part_count > new_part_count) // Some parts are being deleted, so destroy their icons. See other notes in GuiType::Destroy() for explanation.
for (LRESULT i = new_part_count; i < old_part_count; ++i) // Verified correct.
if (hicon = (HICON)SendMessage(control_hwnd, SB_GETICON, i, 0))
HICON hicon_old = (HICON)SendMessage(control_hwnd, SB_GETICON, part_index, 0); // Get the old one before setting the new one.
// For code simplicity, the script is responsible for destroying the hicon later, if it ever destroys
// the window. Though in practice, most people probably won't do this, which is usually okay (if the
// script doesn't load too many) since they're all destroyed by the system upon program termination.
if (SendMessage(control_hwnd, SB_SETICON, part_index, (LPARAM)hicon))
{
aResultToken.value_int64 = (__int64)hicon; // Override the 0 default. This allows the script to destroy the HICON later when it doesn't need it (see comments above too).
if (hicon_old)
// Although the old icon is automatically destroyed here, the script can call SendMessage(SB_SETICON)
// itself if it wants to work with HICONs directly (for performance reasons, etc.)
DestroyIcon(hicon_old);
}
else
DestroyIcon(hicon);
//And leave aResultToken.value_int64 at its default value.
}
//else can't load icon, so leave aResultToken.value_int64 at its default value.
break;
// SB_SetTipText() not implemented (though can be done via SendMessage in the script) because the conditions
// under which tooltips are displayed don't seem like something a script would want very often:
// This ToolTip text is displayed in two situations:
// When the corresponding pane in the status bar contains only an icon.
// When the corresponding pane in the status bar contains text that is truncated due to the size of the pane.
// In spite of the above, SB_SETTIPTEXT doesn't actually seem to do anything, even when the text is too long
// to fit in a narrowed part, tooltip text has been set, and the user hovers the cursor over the bar. Maybe
// I'm not doing it right or maybe this feature is somehow disabled under certain service packs or conditions.
//case 'T': // SB_SetTipText()
// break;
} // switch(mode)
}
void BIF_LV_GetNextOrCount(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// LV_GetNext:
// Returns: The index of the found item, or 0 on failure.
// Parameters:
// 1: Starting index (one-based when it comes in). If absent, search starts at the top.
// 2: Options string.
// 3: (FUTURE): Possible for use with LV_FindItem (though I think it can only search item text, not subitem text).
{
bool mode_is_count = toupper(aResultToken.marker[6]) == 'C'; // Union's marker initially contains the function name. LV_Get[C]ount. Bug-fixed for v1.0.43.09.
char *buf = aResultToken.buf; // Must be saved early since below overwrites the union (better maintainability too).
aResultToken.value_int64 = 0; // Set default return value. Must be done only after consulting marker above.
// Above sets default result in case of early return. For code reduction, a zero is returned for all
// the following conditions:
// Window doesn't exist.
// Control doesn't exist (i.e. no ListView in window).
// Item not found in ListView.
if (!g_gui[g.GuiDefaultWindowIndex])
return;
GuiType &gui = *g_gui[g.GuiDefaultWindowIndex]; // Always operate on thread's default window to simplify the syntax.
// To retain compatibility in the future, also allow "Check(ed)" and "Focus(ed)" since any word that
// starts with C or F is already supported.
switch(first_char)
{
case '\0': // Listed first for performance.
case 'F':
aResultToken.value_int64 = ListView_GetNextItem(control_hwnd, index
, first_char ? LVNI_FOCUSED : LVNI_SELECTED) + 1; // +1 to convert to 1-based.
break;
case 'C': // Checkbox: Find checked items. For performance assume that the control really has checkboxes.
int item_count = ListView_GetItemCount(control_hwnd);
for (int i = index + 1; i < item_count; ++i) // Start at index+1 to omit the first item from the search (for consistency with the other mode above).
if (ListView_GetCheckState(control_hwnd, i)) // Item's box is checked.
{
aResultToken.value_int64 = i + 1; // +1 to convert from zero-based to one-based.
return;
}
// Since above didn't return, no match found. The 0/false value previously set as the default is retained.
break;
}
}
void BIF_LV_GetText(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Returns: 1 on success and 0 on failure.
// Parameters:
// 1: Output variable (doing it this way allows success/fail return value to more closely mirror the API and
// simplifies the code since there is currently no easy means of passing back large strings to our caller).
// 2: Row index (one-based when it comes in).
// 3: Column index (one-based when it comes in).
{
aResultToken.value_int64 = 0; // Set default return value.
// Above sets default result in case of early return. For code reduction, a zero is returned for all
// the following conditions:
// Window doesn't exist.
// Control doesn't exist (i.e. no ListView in window).
// Item not found in ListView.
// And others.
if (!g_gui[g.GuiDefaultWindowIndex])
return;
GuiType &gui = *g_gui[g.GuiDefaultWindowIndex]; // Always operate on thread's default window to simplify the syntax.
if (!gui.mCurrentListView)
return;
// Caller has ensured there is at least two parameters:
if (aParam[0]->symbol != SYM_VAR) // No output variable. Supporting a NULL for the purpose of checking for the existence of a cell seems too rarely needed.
return;
// Caller has ensured there is at least two parameters.
int row_index = (int)ExprTokenToInt64(*aParam[1]) - 1; // -1 to convert to zero-based.
// If parameter 3 is omitted, default to the first column (index 0):
int col_index = (aParamCount > 2) ? (int)ExprTokenToInt64(*aParam[2]) - 1 : 0; // -1 to convert to zero-based.
if (row_index < -1 || col_index < 0) // row_index==-1 is reserved to mean "get column heading's text".
return;
Var &output_var = *aParam[0]->var; // It was already ensured higher above that symbol==SYM_VAR.
char buf[LV_TEXT_BUF_SIZE];
if (row_index == -1) // Special mode to get column's text.
{
LVCOLUMN lvc;
lvc.cchTextMax = LV_TEXT_BUF_SIZE - 1; // See notes below about -1.
lvc.pszText = buf;
lvc.mask = LVCF_TEXT;
if (aResultToken.value_int64 = SendMessage(gui.mCurrentListView->hwnd, LVM_GETCOLUMN, col_index, (LPARAM)&lvc)) // Assign.
output_var.Assign(lvc.pszText); // See notes below about why pszText is used instead of buf (might apply to this too).
else // On failure, it seems best to also clear the output var for better consistency and in case the script doesn't check the return value.
output_var.Assign();
}
else // Get row's indicated item or subitem text.
{
LVITEM lvi;
// Subtract 1 because of that nagging doubt about size vs. length. Some MSDN examples subtract one, such as
// TabCtrl_GetItem()'s cchTextMax:
lvi.iItem = row_index;
lvi.iSubItem = col_index; // Which field to fetch. If it's zero, the item vs. subitem will be fetched.
lvi.mask = LVIF_TEXT;
lvi.pszText = buf;
lvi.cchTextMax = LV_TEXT_BUF_SIZE - 1; // Note that LVM_GETITEM doesn't update this member to reflect the new length.
// Unlike LVM_GETITEMTEXT, LVM_GETITEM indicates success or failure, which seems more useful/preferable
// as a return value since a text length of zero would be ambiguous: could be an empty field or a failure.
if (aResultToken.value_int64 = SendMessage(gui.mCurrentListView->hwnd, LVM_GETITEM, 0, (LPARAM)&lvi)) // Assign
// Must use lvi.pszText vs. buf because MSDN says: "Applications should not assume that the text will
// necessarily be placed in the specified buffer. The control may instead change the pszText member
// of the structure to point to the new text rather than place it in the buffer."
output_var.Assign(lvi.pszText);
else // On failure, it seems best to also clear the output var for better consistency and in case the script doesn't check the return value.
output_var.Assign();
}
}
void BIF_LV_AddInsertModify(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Returns: 1 on success and 0 on failure.
// Parameters:
// 1: For Add(), this is the options. For Insert/Modify, it's the row index (one-based when it comes in).
// 2: For Add(), this is the first field's text. For Insert/Modify, it's the options.
// 3 and beyond: Additional field text.
// In Add/Insert mode, if there are no text fields present, a blank for is appended/inserted.
{
char mode = toupper(aResultToken.marker[3]); // Union's marker initially contains the function name. e.g. LV_[I]nsert.
char *buf = aResultToken.buf; // Must be saved early since below overwrites the union (better maintainability too).
aResultToken.value_int64 = 0; // Set default return value. Must be done only after consulting marker above.
// Above sets default result in case of early return. For code reduction, a zero is returned for all
// the following conditions:
// Window doesn't exist.
// Control doesn't exist (i.e. no ListView in window).
// And others as shown below.
int index;
if (mode == 'A') // For Add mode (A), use INT_MAX as a signal to append the item rather than inserting it.
{
index = INT_MAX;
mode = 'I'; // Add has now been set up to be the same as insert, so change the mode to simplify other things.
}
else // Insert or Modify: the target row-index is their first parameter, which load-time has ensured is present.
{
index = (int)ExprTokenToInt64(*aParam[0]) - 1; // -1 to convert to zero-based.
if (index < -1 || (mode != 'M' && index < 0)) // Allow -1 to mean "all rows" when in modify mode.
return;
++aParam; // Remove the first parameter from further consideration to make Insert/Modify symmetric with Add.
--aParamCount;
}
if (!g_gui[g.GuiDefaultWindowIndex])
return;
GuiType &gui = *g_gui[g.GuiDefaultWindowIndex]; // Always operate on thread's default window to simplify the syntax.
lvi.mask = LVIF_STATE; // LVIF_STATE: state member is valid, but only to the extent that corresponding bits are set in stateMask (the rest will be ignored).
lvi.stateMask = 0;
lvi.state = 0;
// Parse list of space-delimited options:
char *next_option, *option_end, orig_char;
bool adding; // Whether this option is beeing added (+) or removed (-).
for (next_option = options; *next_option; next_option = omit_leading_whitespace(option_end))
{
if (*next_option == '-')
{
adding = false;
// omit_leading_whitespace() is not called, which enforces the fact that the option word must
// immediately follow the +/- sign. This is done to allow the flexibility to have options
// omit the plus/minus sign, and also to reserve more flexibility for future option formats.
++next_option; // Point it to the option word itself.
}
else
{
// Assume option is being added in the absence of either sign. However, when we were
// called by GuiControl(), the first option in the list must begin with +/- otherwise the cmd
// would never have been properly detected as GUICONTROL_CMD_OPTIONS in the first place.
adding = true;
if (*next_option == '+')
++next_option; // Point it to the option word itself.
//else do not increment, under the assumption that the plus has been omitted from a valid
// option word and is thus an implicit plus.
}
if (!*next_option) // In case the entire option string ends in a naked + or -.
break;
// Find the end of this option item:
if ( !(option_end = StrChrAny(next_option, " \t")) ) // Space or tab.
option_end = next_option + strlen(next_option); // Set to position of zero terminator instead.
if (option_end == next_option)
continue; // i.e. the string contains a + or - with a space or tab after it, which is intentionally ignored.
// Temporarily terminate to help eliminate ambiguity for words contained inside other words,
// such as "Checked" inside of "CheckedGray":
orig_char = *option_end;
*option_end = '\0';
if (!strnicmp(next_option, "Select", 6)) // Could further allow "ed" suffix by checking for that inside, but "Selected" is getting long so it doesn't seem something many would want to use.
{
next_option += 6;
// If it's Select0, invert the mode to become "no select". This allows a boolean variable
// to be more easily applied, such as this expression: "Select" . VarContainingState
if (*next_option && !ATOI(next_option))
adding = !adding;
// Another reason for not having "Select" imply "Focus" by default is that it would probably
// reduce performance when selecting all or a large number of rows.
// Because a row might or might not have focus, the script may wish to retain its current
// focused state. For this reason, "select" does not imply "focus", which allows the
// LVIS_FOCUSED bit to be omitted from the stateMask, which in turn retains the current
// focus-state of the row rather than disrupting it.
lvi.stateMask |= LVIS_SELECTED;
if (adding)
lvi.state |= LVIS_SELECTED;
//else removing, so the presence of LVIS_SELECTED in the stateMask above will cause it to be de-selected.
}
else if (!strnicmp(next_option, "Focus", 5))
{
next_option += 5;
if (*next_option && !ATOI(next_option)) // If it's Focus0, invert the mode to become "no focus".
adding = !adding;
lvi.stateMask |= LVIS_FOCUSED;
if (adding)
lvi.state |= LVIS_FOCUSED;
//else removing, so the presence of LVIS_FOCUSED in the stateMask above will cause it to be de-focused.
}
else if (!strnicmp(next_option, "Check", 5))
{
// The rationale for not checking for an optional "ed" suffix here and incrementing next_option by 2
// is that: 1) It would be inconsistent with the lack of support for "selected" (see reason above);
// 2) Checkboxes in a ListView are fairly rarely used, so code size reduction might be more important.
next_option += 5;
if (*next_option && !ATOI(next_option)) // If it's Check0, invert the mode to become "unchecked".
adding = !adding;
if (mode == 'M') // v1.0.46.10: Do this section only for Modify, not Add/Insert, to avoid generating an extra "unchecked" notification when a row is added/inserted with an initial state of "checked". In other words, the script now receives only a "checked" notification, not an "unchecked+checked". Search on is_checked for more comments.
{
lvi.stateMask |= LVIS_STATEIMAGEMASK;
lvi.state |= adding ? 0x2000 : 0x1000; // The #1 image is "unchecked" and the #2 is "checked".
}
is_checked = adding;
}
else if (!strnicmp(next_option, "Col", 3))
{
if (adding)
{
col_start_index = ATOI(next_option + 3) - 1; // The ability start start at a column other than 1 (i.e. subitem vs. item).
if (col_start_index < 0)
col_start_index = 0;
}
}
else if (!strnicmp(next_option, "Icon", 4))
{
// Testing shows that there is no way to avoid having an item icon in report view if the
// ListView has an associated small-icon ImageList (well, perhaps you could have it show
// a blank square by specifying an invalid icon index, but that doesn't seem useful).
// If LVIF_IMAGE is entirely omitted when adding and item/row, the item will take on the
// first icon in the list. This is probably by design because the control wants to make
// each item look consistent by indenting its first field by a certain amount for the icon.
if (adding)
{
lvi.mask |= LVIF_IMAGE;
lvi.iImage = ATOI(next_option + 4) - 1; // -1 to convert to zero-based.
}
//else removal of icon currently not supported (see comment above), so do nothing in order
// to reserve "-Icon" in case a future way can be found to do it.
}
else if (!stricmp(next_option, "Vis")) // v1.0.44
// Since this option much more typically used with LV_Modify than LV_Add/Insert, the technique of
// Vis%VarContainingOneOrZero% isn't supported, to reduce code size.
ensure_visible = adding; // Ignored by modes other than LV_Modify(), since it's not really appropriate when adding a row (plus would add code complexity).
// If the item was not handled by the above, ignore it because it is unknown.
*option_end = orig_char; // Undo the temporary termination because the caller needs aOptions to be unaltered.
}
// More maintainable and performs better to have a separate struct for subitems vs. items.
LVITEM lvi_sub;
// Ensure mask is pure to avoid giving it any excuse to fail due to the fact that
// "You cannot set the state or lParam members for subitems."
lvi_sub.mask = LVIF_TEXT;
int i, j, rows_to_change;
if (index == -1) // Modify all rows (above has ensured that this is only happens in modify-mode).
ensure_visible = false; // Not applicable when operating on all rows.
}
else // Modify or insert a single row. Set it up for the loop to perform exactly one iteration.
{
rows_to_change = 1;
lvi.iItem = index; // Which row to operate upon. This can be a huge number such as 999999 if the caller wanted to append vs. insert.
}
lvi.iSubItem = 0; // Always zero to operate upon the item vs. sub-item (subitems have their own LVITEM struct).
aResultToken.value_int64 = 1; // Set default from this point forward to be true/success. It will be overridden in insert mode to be the index of the new row.
for (j = 0; j < rows_to_change; ++j, ++lvi.iItem) // ++lvi.iItem because if the loop has more than one iteration, by definition it is modifying all rows starting at 0.
{
if (aParamCount > 1 && col_start_index == 0) // 2nd parameter: item's text (first field) is present, so include that when setting the item.
{
lvi.pszText = ExprTokenToString(*aParam[1], buf); // Fairly low-overhead, so called every iteration for simplicity (so that buf can be used for both items and subitems).
lvi.mask |= LVIF_TEXT;
}
if (mode == 'I') // Insert or Add.
{
// Note that ListView_InsertItem() will append vs. insert if the index is too large, in which case
// it returns the items new index (which will be the last item in the list unless the control has
// auto-sort style).
// Below uses +1 to convert from zero-based to 1-based. This also converts a failure result of -1 to 0.
if ( !(aResultToken.value_int64 = ListView_InsertItem(control.hwnd, &lvi) + 1) )
return; // Since item can't be inserted, no reason to try attaching any subitems to it.
// Update iItem with the actual index assigned to the item, which might be different than the
// specified index if the control has an auto-sort style in effect. This new iItem value
// is used for ListView_SetCheckState() and for the attaching of any subitems to this item.
lvi_sub.iItem = (int)aResultToken.value_int64 - 1; // -1 to convert back to zero-based.
// For add/insert (but not modify), testing shows that checkmark must be added only after
// the item has been inserted rather than provided in the lvi.state/stateMask fields.
// MSDN confirms this by saying "When an item is added with [LVS_EX_CHECKBOXES],
// it will always be set to the unchecked state [ignoring any value placed in bits
// 12 through 15 of the state member]."
if (is_checked)
ListView_SetCheckState(control.hwnd, lvi_sub.iItem, TRUE); // TRUE = Check the row's checkbox.
// Note that 95/NT4 systems that lack comctl32.dll 4.70+ distributed with MSIE 3.x
// do not support LVS_EX_CHECKBOXES, so the above will have no effect for them.
}
else // Modify.
{
// Rather than trying to detect if anything was actually changed, this is called
// unconditionally to simplify the code (ListView_SetItem() is probably very fast if it
// discovers that lvi.mask==LVIF_STATE and lvi.stateMask==0).
// By design (to help catch script bugs), a failure here does not revert to append mode.
if (!ListView_SetItem(control.hwnd, &lvi)) // Returns TRUE/FALSE.
aResultToken.value_int64 = 0; // Indicate partial failure, but attempt to continue in case it failed for reason other than "row doesn't exist".
lvi_sub.iItem = lvi.iItem; // In preparation for modifying any subitems that need it.
if (ensure_visible) // Seems best to do this one prior to "select" below.
SendMessage(control.hwnd, LVM_ENSUREVISIBLE, lvi.iItem, FALSE); // PartialOK==FALSE is somewhat arbitrary.
}
// For each remainining parameter, assign its text to a subitem.
// Testing shows that if the control has too few columns for all of the fields/parameters
// present, the ones at the end are automatically ignored: they do not consume memory nor
// do they signficantly impact performance (at least on Windows XP). For this reason, there
// is no code above the for-loop above to reduce aParamCount if it's "too large" because
// it might reduce flexibility (in case future/past OSes allow non-existent columns to be
// populated, or in case current OSes allow the contents of recently removed columns to be modified).
for (lvi_sub.iSubItem = (col_start_index > 1) ? col_start_index : 1 // Start at the first subitem unless we were told to start at or after the third column.
// "i" starts at 2 (the third parameter) unless col_start_index is greater than 0, in which case
// it starts at 1 (the second parameter) because that parameter has not yet been assigned to anything:
, i = 2 - (col_start_index > 0)
; i < aParamCount
; ++i, ++lvi_sub.iSubItem)
if (lvi_sub.pszText = ExprTokenToString(*aParam[i], buf)) // Done every time through the outer loop since it's not high-overhead, and for code simplicity.
if (!ListView_SetItem(control.hwnd, &lvi_sub) && mode != 'I') // Relies on short-circuit. Seems best to avoid loss of item's index in insert mode, since failure here should be rare.
aResultToken.value_int64 = 0; // Indicate partial failure, but attempt to continue in case it failed for reason other than "row doesn't exist".
// else not an operand, but it's simplest just to try to continue.
} // outer for()
// When the control has no rows, work around the fact that LVM_SETITEMCOUNT delivers less than 20%
// of its full benefit unless done after the first row is added (at least on XP SP1). A non-zero
// row_count_hint tells us that this message should be sent after the row has been inserted/appended:
if (control.union_lv_attrib->row_count_hint > 0 && mode == 'I')
{
SendMessage(control.hwnd, LVM_SETITEMCOUNT, control.union_lv_attrib->row_count_hint, 0); // Last parameter should be 0 for LVS_OWNERDATA (verified if you look at the definition of ListView_SetItemCount macro).
control.union_lv_attrib->row_count_hint = 0; // Reset so that it only gets set once per request.
}
}
void BIF_LV_Delete(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Returns: 1 on success and 0 on failure.
// Parameters:
// 1: Row index (one-based when it comes in).
{
aResultToken.value_int64 = 0; // Set default return value.
// Above sets default result in case of early return. For code reduction, a zero is returned for all
// the following conditions:
// Window doesn't exist.
// Control doesn't exist (i.e. no ListView in window).
// And others as shown below.
if (!g_gui[g.GuiDefaultWindowIndex])
return;
GuiType &gui = *g_gui[g.GuiDefaultWindowIndex]; // Always operate on thread's default window to simplify the syntax.
else if (!stricmp(next_option, "Logical")) // v1.0.44.12: Supports StrCmpLogicalW() method of sorting.
col.case_sensitive = SCS_INSENSITIVE_LOGICAL;
else if (!strnicmp(next_option, "Sort", 4)) // This is done as an option vs. LV_SortCol/LV_Sort so that the column's options can be changed simultaneously with a "sort now" to refresh.
{
// Defer the sort until after all options have been parsed and applied.
sort_now = true;
if (!stricmp(next_option + 4, "Desc"))
sort_now_direction = 'D'; // Descending.
}
else if (!stricmp(next_option, "NoSort")) // Called "NoSort" so that there's a way to enable and disable the setting via +/-.
col.sort_disabled = adding;
else if (!strnicmp(next_option, "Auto", 4)) // No separate failure result is reported for this item.
// In case the mode is "insert", defer auto-width of column until col exists.
// Since above didn't return, this is TV_Add() or TV_Modify().
TVINSERTSTRUCT tvi; // It contains a TVITEMEX, which is okay even if MSIE pre-4.0 on Win95/NT because those OSes will simply never access the new/bottommost item in the struct.
bool adding; // Whether this option is beeing added (+) or removed (-).
for (next_option = options; *next_option; next_option = omit_leading_whitespace(option_end))
{
if (*next_option == '-')
{
adding = false;
// omit_leading_whitespace() is not called, which enforces the fact that the option word must
// immediately follow the +/- sign. This is done to allow the flexibility to have options
// omit the plus/minus sign, and also to reserve more flexibility for future option formats.
++next_option; // Point it to the option word itself.
}
else
{
// Assume option is being added in the absence of either sign. However, when we were
// called by GuiControl(), the first option in the list must begin with +/- otherwise the cmd
// would never have been properly detected as GUICONTROL_CMD_OPTIONS in the first place.
adding = true;
if (*next_option == '+')
++next_option; // Point it to the option word itself.
//else do not increment, under the assumption that the plus has been omitted from a valid
// option word and is thus an implicit plus.
}
if (!*next_option) // In case the entire option string ends in a naked + or -.
break;
// Find the end of this option item:
if ( !(option_end = StrChrAny(next_option, " \t")) ) // Space or tab.
option_end = next_option + strlen(next_option); // Set to position of zero terminator instead.
if (option_end == next_option)
continue; // i.e. the string contains a + or - with a space or tab after it, which is intentionally ignored.
// Temporarily terminate to help eliminate ambiguity for words contained inside other words,
// such as "Checked" inside of "CheckedGray":
orig_char = *option_end;
*option_end = '\0';
if (!stricmp(next_option, "Select")) // Could further allow "ed" suffix by checking for that inside, but "Selected" is getting long so it doesn't seem something many would want to use.
{
// Selection of an item apparently needs to be done via message for the control to update itself
// properly. Otherwise, single-select isn't enforced via de-selecitng previous item and the newly
// selected item isn't revealed/shown. There may be other side-effects.
if (adding)
select_flag = TVGN_CARET;
//else since "de-select" is not a supported action, no need to support "-Select".
// Furthermore, since a TreeView is by its nature has only one item selected at a time, it seems
// unnecessary to support Select%VarContainingOneOrZero%. This is because it seems easier for a
// script to simply load the Tree then select the desired item afterward.
}
else if (!strnicmp(next_option, "Vis", 3))
{
// Since this option much more typically used with TV_Modify than TV_Add, the technique of
// Vis%VarContainingOneOrZero% isn't supported, to reduce code size.
next_option += 3;
if (!stricmp(next_option, "First")) // VisFirst
ensure_visible_first = adding;
else if (!*next_option)
ensure_visible = adding;
}
else if (!stricmp(next_option, "Bold"))
{
// Bold%VarContainingOneOrZero isn't supported because due to rarity. There might someday
// be a ternary operator to make such things easier anyway.
tvi.item.stateMask |= TVIS_BOLD;
if (adding)
tvi.item.state |= TVIS_BOLD;
//else removing, so the fact that this TVIS flag has just been added to the stateMask above
// but is absent from item.state should remove this attribute from the item.
}
else if (!strnicmp(next_option, "Expand", 6))
{
next_option += 6;
if (*next_option && !ATOI(next_option)) // If it's Expand0, invert the mode to become "collapse".
adding = !adding;
if (add_mode)
{
if (adding)
{
// Don't expand via msg because it won't work: since the item is being newly added
// now, by definition it doesn't have any children, and testing shows that sending
// the expand message has no effect, but setting the state bit does:
tvi.item.stateMask |= TVIS_EXPANDED;
tvi.item.state |= TVIS_EXPANDED;
// Since the script is deliberately expanding the item, it seems best not to send the
// TVN_ITEMEXPANDING/-ED messages because:
// 1) Sending TVN_ITEMEXPANDED without first sending a TVN_ITEMEXPANDING message might
// decrease maintainability, and possibly even produce unwanted side-effects.
// 2) Code size and performance (avoids generating extra message traffic).
}
//else removing, so nothing needs to be done because "collapsed" is the default state
// of a TV item upon creation.
}
else // TV_Modify(): Expand and collapse both require a message to work properly on an existing item.
// Strangely, this generates a notification sometimes (such as the first time) but not for subsequent
// expands/collapses of that same item. Also, TVE_TOGGLE is not currently supported because it seems
// like it would be too rarely used.
if (!TreeView_Expand(control.hwnd, tvi.item.hItem, adding ? TVE_EXPAND : TVE_COLLAPSE))
aResultToken.value_int64 = 0; // Indicate partial failure by overriding the HTREEITEM return value set earlier.
// It seems that despite what MSDN says, failure is returned when collapsing and item that is
// already collapsed, but not when expanding an item that is already expanded. For performance
// reasons and rarity of script caring, it seems best not to try to adjust/fix this.
}
else if (!strnicmp(next_option, "Check", 5))
{
// The rationale for not checking for an optional "ed" suffix here and incrementing next_option by 2
// is that: 1) It would be inconsistent with the lack of support for "selected" (see reason above);
// 2) Checkboxes in a ListView are fairly rarely used, so code size reduction might be more important.
next_option += 5;
if (*next_option && !ATOI(next_option)) // If it's Check0, invert the mode to become "unchecked".
adding = !adding;
//else removing, so the fact that this TVIS flag has just been added to the stateMask above
// but is absent from item.state should remove this attribute from the item.
tvi.item.stateMask |= TVIS_STATEIMAGEMASK; // Unlike ListViews, Tree checkmarks can be applied in the same step as creating a Tree item.
tvi.item.state |= adding ? 0x2000 : 0x1000; // The #1 image is "unchecked" and the #2 is "checked".
}
else if (!strnicmp(next_option, "Icon", 4))
{
if (adding)
{
// To me, having a different icon for when the item is selected seems rarely used. After all,
// its obvious the item is selected because it's highlighed (unless it lacks a name?) So this
// policy makes things easier for scripts that don't want to distinguish. If ever it is needed,
// new options such as IconSel and IconUnsel can be added.
tvi.item.mask |= TVIF_IMAGE|TVIF_SELECTEDIMAGE;
tvi.item.iSelectedImage = tvi.item.iImage = ATOI(next_option + 4) - 1; // -1 to convert to zero-based.
}
//else removal of icon currently not supported (see comment above), so do nothing in order
// to reserve "-Icon" in case a future way can be found to do it.
}
else if (!stricmp(next_option, "Sort"))
{
if (add_mode)
tvi.hInsertAfter = TVI_SORT; // For simplicity, the value of "adding" is ignored.
else
// Somewhat debatable, but it seems best to report failure via the return value even though
// failure probably only occurs when the item has no children, and the script probably
// doesn't often care about such failures. It does result in the loss of the HTREEITEM return
// value, but even if that call is nested in another, the zero should produce no effect in most cases.
if (!TreeView_SortChildren(control.hwnd, tvi.item.hItem, FALSE)) // Best default seems no-recurse, since typically this is used after a user edits merely a single item.
aResultToken.value_int64 = 0; // Indicate partial failure by overriding the HTREEITEM return value set earlier.
}
else if (add_mode) // MUST BE LISTED LAST DUE TO "ELSE IF": Options valid only for TV_Add().
{
if (!stricmp(next_option, "First"))
tvi.hInsertAfter = TVI_FIRST; // For simplicity, the value of "adding" is ignored.
else if (IsPureNumeric(next_option, false, false, false))
tvi.hInsertAfter = (HTREEITEM)ATOI64(next_option); // ATOI64 vs. ATOU avoids need for extra casting to avoid compiler warning.
}
//else some unknown option, just ignore it.
// If the item was not handled by the above, ignore it because it is unknown.
*option_end = orig_char; // Undo the temporary termination because the caller needs aOptions to be unaltered.
tvi.item.hItem = TreeView_InsertItem(control.hwnd, &tvi); // Update tvi.item.hItem for convenience/maint. I'ts for use in later sections because aResultToken.value_int64 is overridden to be zero for partial failure in modify-mode.
aResultToken.value_int64 = (__int64)tvi.item.hItem; // Set return value.
}
else // TV_Modify()
{
if (aParamCount > 2) // An explicit empty string is allowed, which sets it to a blank value. By contrast, if the param is omitted, the name is left changed.
{
tvi.item.pszText = ExprTokenToString(*aParam[2], buf); // Reuse buf now that options (above) is done with it.
tvi.item.mask |= TVIF_TEXT;
}
//else name/text parameter has been omitted, so don't change the item's name.
if (tvi.item.mask != LVIF_STATE || tvi.item.stateMask) // An item's property or one of the state bits needs changing.
if (!TreeView_SetItem(control.hwnd, &tvi.itemex))
aResultToken.value_int64 = 0; // Indicate partial failure by overriding the HTREEITEM return value set earlier.
}
if (ensure_visible) // Seems best to do this one prior to "select" below.
SendMessage(control.hwnd, TVM_ENSUREVISIBLE, 0, (LPARAM)tvi.item.hItem); // Return value is ignored in this case, since its definition seems a little weird.
if (ensure_visible_first) // Seems best to do this one prior to "select" below.
TreeView_Select(control.hwnd, tvi.item.hItem, TVGN_FIRSTVISIBLE); // Return value is also ignored due to rarity, code size, and because most people wouldn't care about a failure even if for some reason it failed.
if (select_flag)
if (!TreeView_Select(control.hwnd, tvi.item.hItem, select_flag) && !add_mode) // Relies on short-circuit boolean order.
aResultToken.value_int64 = 0; // When not in add-mode, indicate partial failure by overriding the return value set earlier (add-mode should always return the new item's ID).
case 'E': state_mask = TVIS_EXPANDED; break; // Expanded
case 'C': state_mask = TVIS_STATEIMAGEMASK; break; // Checked
case 'B': state_mask = TVIS_BOLD; break; // Bold
//case 'S' for "Selected" is not provided because TV_GetSelection() seems to cover that well enough.
//case 'P' for "is item a parent?" is not provided because TV_GetChild() seems to cover that well enough.
// (though it's possible that retrieving TVITEM's cChildren would perform a little better).
}
// Below seems to be need a bit-AND with state_mask to work properly, at least on XP SP2. Otherwise,
// extra bits are present such as 0x2002 for "expanded" when it's supposed to be either 0x00 or 0x20.
UINT result = state_mask & (UINT)SendMessage(control_hwnd, TVM_GETITEMSTATE, (WPARAM)hitem, state_mask);
if (state_mask == TVIS_STATEIMAGEMASK)
{
if (result == 0x2000) // It has a checkmark state image.
aResultToken.value_int64 = (size_t)hitem; // Override 0 set earlier. More useful than returning 1 since it allows function-call nesting.
}
else // For all others, anything non-zero means the flag is present.
if (result)
aResultToken.value_int64 = (size_t)hitem; // Override 0 set earlier. More useful than returning 1 since it allows function-call nesting.
return;
}
// Since above didn't return, this is LV_GetText().
// Loadtime validation has ensured that param #1 and #2 are present.
if (aParam[0]->symbol != SYM_VAR) // No output variable. Supporting a NULL for the purpose of checking for the existence of an item seems too rarely needed.
return;
Var &output_var = *aParam[0]->var;
char text_buf[LV_TEXT_BUF_SIZE]; // i.e. uses same size as ListView.
tvi.cchTextMax = LV_TEXT_BUF_SIZE - 1; // -1 because of nagging doubt about size vs. length. Some MSDN examples subtract one), such as TabCtrl_GetItem()'s cchTextMax.
if (SendMessage(control_hwnd, TVM_GETITEM, 0, (LPARAM)&tvi))
{
// Must use tvi.pszText vs. text_buf because MSDN says: "Applications should not assume that the text will
// necessarily be placed in the specified buffer. The control may instead change the pszText member
// of the structure to point to the new text rather than place it in the buffer."
output_var.Assign(tvi.pszText);
aResultToken.value_int64 = (size_t)tvi.hItem; // More useful than returning 1 since it allows function-call nesting.
}
else // On failure, it seems best to also clear the output var for better consistency and in case the script doesn't check the return value.
output_var.Assign();
// And leave aResultToken.value_int64 set to its default of 0.
}
void BIF_IL_Create(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount)
// Returns: Handle to the new image list, or 0 on failure.
// Parameters:
// 1: Initial image count (ImageList_Create() ignores values <=0, so no need for error checking).
// 2: Grow count (testing shows it can grow multiple times, even when this is set <=0, so it's apparently only a performance aid)
// 3: Width of each image (overloaded to mean small icon size when omitted or false, large icon size otherwise).
// 4: Future: Height of each image [if this param is present and >0, it would mean param 3 is not being used in its TRUE/FALSE mode)
// 5: Future: Flags/Color depth
{
// So that param3 can be reserved as a future "specified width" param, to go along with "specified height"
// after it, only when the parameter is both present and numerically zero are large icons used. Otherwise,
// small icons are used.
int param3 = aParamCount > 2 ? (int)ExprTokenToInt64(*aParam[2]) : 0;
, ILC_MASK | ILC_COLOR32 // ILC_COLOR32 or at least something higher than ILC_COLOR is necessary to support true-color icons.
, aParamCount > 0 ? (int)ExprTokenToInt64(*aParam[0]) : 2 // cInitial. 2 seems a better default than one, since it might be common to have only two icons in the list.